Search
moon
sun

ArgoCD 앱 전환 트러블슈팅: 소유권 중복, Cascade Delete, Finalizer

flink session mode에서 application 모드로의 이관 진행중에 기존에 운영하던 ArgoCD Application을 새 Application으로 교체하는 작업을 했다. 새 앱을 연결하고 기존 앱을 삭제했는데 그 직후 네임스페이스의 Flink 잡 전체가 재배포됐고 잡 하나는 삭제가 완료되지 않은 채 며칠간 방치됐다. 이번 글은 이 과정에서 이해하게 된 ArgoCD의 리소스 소유권 모델, cascade 삭제, 그리고 finalizer가 삭제를 막는 메커니즘을 정리한다.

ArgoCD의 상태 비교 모델

ArgoCD는 두 상태를 계속 비교한다.
•
desired state: git에 선언된 매니페스트 (helm이면 렌더된 결과)
•
live state: 클러스터에 실제 존재하는 리소스
sync는 desired를 live에 apply해서 둘을 일치시키는 행위로 여기에는 세 가지 옵션이 있다.
옵션
동작
비활성 시
auto-sync (syncPolicy.automated)
desired 변경 시 자동 sync
사람이 수동으로 sync (manual-sync)
self-heal
live가 desired에서 벗어나면 자동 복원
드리프트 방치
prune
desired에서 사라진 리소스를 sync 시 삭제
orphan으로 잔존
self-heal과 prune은 auto-sync가 켜져 있어야 동작한다. 이 경우 싱크 되지 않은 리소스는 OutOfSync 상태로 머물게 된다.
우리팀에서는 마지막 배포과정은 개발자가 직접 확인하고 sync할 수 있게 manual-sync 로만 작업하고 있다.

리소스 소유권: tracking

ArgoCD는 자기가 배포한 리소스에 소유권 표식을 남긴다. argocd-cm(argocd 네임스페이스의 전역 설정 ConfigMap)의 application.resourceTrackingMethod로 결정되며 두가지 방식이 있다.
구분
Label 방식
Annotation 방식
키
app.kubernetes.io/instance
argocd.argoproj.io/tracking-id
담는 정보
앱 이름만
앱 이름 + 리소스 좌표 (group/kind/ns/name)
충돌
instance는 Helm 등 다른 도구에서도 사용하는 범용 라벨 → 차트가 자체 설정하면 추적 오염 가능
Argo CD 전용 키라 충돌 가능성 낮음
오탐
이름만 매칭 → 같은 앱 이름을 가진 복사 리소스도 소유로 오인 가능
좌표까지 검증 → 해당 앱이 해당 위치에 생성한 리소스만 정확히 매칭
이 표식이 중요한 이유는 ArgoCD의 세 가지 (diff, prune, cascade) 동작이 전부 이걸 기준으로 대상을 판정하기 때문이다.
구분
Diff
Prune
Cascade Delete
하는 일
Desired와 Live의 차이 계산
Sync 시 Desired에서 사라진 리소스 삭제
앱 삭제 시 소유 리소스 전부 삭제
트리거
상시 (Refresh마다)
Sync 시 + prune 옵션
앱 Delete 시 + resources-finalizer
기준
—
Git에서 리소스가 빠짐
Argo CD App 자체가 삭제됨
영향 범위
읽기 전용
리소스 단위
앱 단위
결과
OutOfSync 표시
리소스 삭제, 앱은 유지
앱과 소유 리소스 삭제
문제는 한 리소스가 두 앱의 desired에 동시에 들어 있는 경우.
표식은 하나뿐이므로 마지막에 sync한 앱이 소유권을 가져가서, 겉으로는 양쪽 다 Synced로 보이지만 실제 소유권은 한쪽에만 걸려있다.

앱 삭제: cascade와 orphan

Application 삭제 시 동작은 앱 CR에 resources-finalizer.argocd.argoproj.io finalizer가 있는지에 따라 갈린다.
# Application CR kind: Application metadata: name: prod-flink finalizers: - resources-finalizer.argocd.argoproj.io # ← 이게 있으면 cascade
JavaScript
복사
•
finalizer 있음 → cascade delete: 앱이 소유한 모든 리소스를 함께 삭제
•
finalizer 없음 → orphan delete: 앱 객체만 삭제, 리소스는 클러스터에 잔존
UI에서 삭제하면 cascade가 기본 선택이어서 삭제 대상은 위의 소유권 표식 기준으로 결정된다.

RBAC 삭제가 워크로드를 무너뜨리는 경로

SA/Role 삭제가 왜 파드 재시작으로 이어지는지는 Flink의 HA 구조를 봐야 한다.
Flink Kubernetes HA(high-availability: kubernetes)에서 JobManager는
•
leader election과 메타데이터를 ConfigMap에 쓰고 읽는다
•
TaskManager 파드를 API 호출로 직접 생성한다
모든 동작이 잡에 부여된 SA + Role 권한에 의존하기 때문에 RBAC이 사라지면,
1.
JM의 configmap 접근이 403으로 거부됨
2.
leader lease 갱신 실패 → leadership 상실
3.
JM 크래시 → job 전체 페일오버
이후 재싱크로 RBAC을 복구하면 operator가 FlinkDeployment를 다시 reconcile하면서 잡들이 fresh 재배포된다. RBAC은 leader election이 configmap 기반인 워크로드에게는 런타임 의존성이다.

앱 삭제가 끝나지 않는 이유: finalizer 체인

각 리소스에는 담당 컨트롤러가 자신의 finalizer(metadata.finalizers[])를 붙여둔다
finalizer는 실제 삭제 전에 처리할 뒷정리가 있다는 표식이다. 이 표식이 있는 객체에 delete를 걸면:
1.
k8s는 바로 지우지 않고 deletionTimestamp만 기록 → Terminating 상태로 대기
2.
담당 컨트롤러가 뒷정리를 끝내고 finalizer를 제거
3.
그제야 실제 삭제
구분
Application
FlinkDeployment
Finalizer 키
resources-finalizer.argocd.argoproj.io
flinkdeployments.flink.apache.org/finalizer
붙이는 주체
Argo CD
Flink Operator
뒷정리 내용
소유 리소스 전부 삭제 (Cascade)
Job Cancel + HA 정리
제거 주체
Argo CD (Cascade 완료 후)
Flink Operator (정리 완료 후)
정리 실패 시
Finalizer가 남아 Terminating 상태 유지
Finalizer가 남아 Terminating 상태 유지
컨트롤러가 뒷정리를 완결하지 못하면 finalizer가 남고 리소스는 Terminating에 영구히 머문다.
💡
알아둘 부분
•
deletionTimestamp가 찍힌 리소스는 컨트롤러가 삭제 중으로 취급하므로 재싱크해도 되살아나지 않음
•
복구 방법: finalizer 수동 제거 → 삭제 완결 → 재생성
•
단 operator의 정상 종료 절차(savepoint 등)를 건너뛰는 조작 → 상태 유실 감수 필요

이번 사고의 흐름

앞의 개념들을 시간순으로 연결하면 사고의 전체 흐름이 나온다.
1.
이중 관리 상태
•
ServiceAccount / Role / RoleBinding / PodMonitor 같은 네임스페이스 공유 인프라가 Flink 잡(FlinkDeployment)들과 같은 helm 차트에서 렌더됨
•
이 차트를 바라보는 앱을 옛 앱 / 새 앱 두 개 생성 → 잡과 인프라 전부가 두 앱의 desired에 동시 포함
2.
옛 앱 cascade 삭제
•
옛 앱 삭제 시 옛 앱이 소유 표식을 갖고 있던 공유 인프라도 함께 삭제
•
새 앱 기준으로는 자기 desired의 리소스가 사라진 것 → 전부 OutOfSync 전환
•
manual-sync 운영이라 자동 복원 없음
3.
RBAC 공백 → 잡 붕괴 → 재싱크로 전체 재배포
•
SA / Role 부재 동안 JM의 configmap 접근 거부 → leader 상실 → 잡 붕괴
•
OutOfSync 수동 재싱크 → 인프라 재생성 (이름은 같지만 새 오브젝트 / 소유권은 새 앱으로 이전)
•
operator가 FlinkDeployment reconcile → 네임스페이스 잡 전체 fresh 재배포
4.
잡 하나가 삭제에 갇힘 — finalizer 체인
flowchart TD
    A[Application 삭제 요청] --> B["resources-finalizer 존재<br/>deletionTimestamp 기록"]
    B --> C[ArgoCD: cascade delete 시작]
    C --> D[FlinkDeployment 삭제 시도]
    D --> E["flink finalizer 존재<br/>deletionTimestamp 기록"]
    E --> F["Flink Operator가 정리 시도<br/>(Job Cancel + HA 정리)"]
    F -->|정리 미완료| G[FlinkDeployment: Terminating stuck]
    G -->|리소스가 안 지워짐| H[ArgoCD: cascade 완료 불가]
    H -->|resources-finalizer 못 풂| I[Application: Deleting stuck]
    I -.기다림.-> G
    G -.기다림.-> I

    style G fill:#f9d5d3,stroke:#c0392b
    style I fill:#f9d5d3,stroke:#c0392b
Mermaid
복사
•
finalizer가 finalizer를 기다리는 이중 대기
•
deletionTimestamp가 찍힌 CR은 재싱크로도 복원 불가 → 복구도 삭제도 안 되는 상태로 잔존
5.
무알림 상태 지속
•
기존 알림: flink_jobmanager_job_uptime 메트릭 10분 이상 소실 시 Job Down 판정
•
Terminating stuck은 잡이 죽은 상태가 아닌 삭제 보류 상태 - JM과 job은 계속 실행 중 (running 판정)
•
uptime 메트릭 정상 노출 → 알림 기준으로는 완전 정상 → 발견까지 며칠 소요
이 상태를 잡으려면 메트릭이 아니라 리소스 자체를 관측해야함
•
CR의 deletionTimestamp 존재 여부
•
FlinkDeployment의 lifecycle state (RECONCILING 고착 등)

정리

ArgoCD 앱 전환 시 확인 할 것
1.
전환 전 소유권 중복 여부 확인
•
새 앱 생성 전 기존 앱과 새 앱의 desired state에 동일 리소스가 동시 포함되는지 확인 (argocd app diff 또는 렌더링된 매니페스트 비교로 확인 가능)
•
특히 SA/Role/RoleBinding/PodMonitor처럼 여러 워크로드가 공유하는 인프라 리소스는 중복 확률 높음
2.
옛 앱 삭제는 cascade가 아닌 orphan으로
•
새 앱의 소유권 정상 인수 여부(Synced 상태 + 리소스 tracking-id) 우선 검증
•
검증 전까지 옛 앱은 cascade delete(기본값) 대신 orphan delete로 삭제 - finalizer 제거 후 앱 객체만 삭제
3.
manual-sync 환경에서 전환 중 OutOfSync 방치 금지
•
self-heal 비활성 시 desired에서 리소스 누락돼도 자동 복원 없음
구조적 개선 사항
공유 인프라와 워크로드 분리
•
SA/RBAC/모니터링 같은 네임스페이스 공유 리소스는 별도 Application으로 분리
•
워크로드(FlinkDeployment 등) 앱과 라이프사이클이 묶이면, 한쪽 삭제 시 다른 쪽까지 연쇄 삭제
모니터링 대상에 리소스 상태 포함
•
flink_jobmanager_job_uptime 같은 애플리케이션 메트릭만으로는 Terminating stuck 탐지 불가 (JM은 계속 실행 중)
•
CR의 deletionTimestamp 존재 여부, FlinkDeployment의 lifecycle state(RECONCILING 고착 등)를 관측 대상에 포함