LabHub
배우기 러닝패스 코스

CRD 와 오퍼레이터 · 부모를 지웠는데 자식이 남았다 · 이론

지울 때 무슨 일이 일어나는가

LabHub 에서 이어서 보기

한 줄 요약

쿠버네티스에는 소유 관계를 세는 장부가 없다. 자식이 부모를 가리키는 ownerReferences 한 줄이 전부이고,
가비지 컬렉터가 그 줄을 믿고 움직인다. 그래서 그 줄을 잘못 쓰면 고장이 조용히 난다.

왜 이 설계가 필요했나

오퍼레이터는 커스텀 리소스 하나를 받아 여러 개의 실제 자원을 만든다. Site 하나에 ConfigMap 두 개,
Service 하나, Secret 하나 같은 식이다. 그럼 Site 를 지울 때 그 넷은 누가 지우나?

컨트롤러가 직접 지우게 하면 간단해 보이지만 문제가 있다. 컨트롤러가 죽어 있는 동안 부모가 지워지면
자식은 영원히 남는다. 컨트롤러를 아예 걷어 내면 그 자원들은 아무도 모르는 쓰레기가 된다. 그래서
쿠버네티스는 정리를 컨트롤러가 아니라 API 서버 쪽의 가비지 컬렉터에게 맡겼고, 그 컬렉터가 읽을
정보를 자식의 메타데이터에 적게 했다.

metadata:  ownerReferences:    - apiVersion: own.labhub.io/v1      kind: Site      name: alpha      uid: 8f1c...            # 진짜 열쇠는 이것      controller: true        # 이 참조가 '관리자' 인가      blockOwnerDeletion: true

네 필드가 필수다apiVersion·kind·name·uid. 그리고 이 중 실제로 부모를 찾는 데 쓰이는 것은
uid 다. 이름은 사람이 읽는 값이고, 가비지 컬렉터는 uid 로 부모의 존재를 확인한다.

어떻게 동작하나

uid 가 틀리면 자식이 지워진다. 컬렉터 입장에서 "그 uid 를 가진 오브젝트가 없다" 는 "부모가 이미
지워졌다" 와 같은 말이다. 그래서 몇 초 안에 자식을 정리한다. 이 성질이 현장에서 사고로 이어지는 전형적인
경로는 이렇다 — 컨트롤러가 부모의 uid 를 캐시해 두었는데 그 사이 부모가 지워졌다 같은 이름으로 다시
만들어진다. 새 부모의 uid 는 다르다. 옛 uid 로 만든 자식은 만들자마자 사라진다.

네임스페이스를 넘는 소유는 없다. 네임스페이스 범위 자식은 같은 네임스페이스의 부모만 가질 수 있고,
클러스터 범위 오브젝트만 예외로 어느 네임스페이스의 자식이든 가질 수 있다. 규칙을 어긴 참조는 apply 에서
막히지 않는다. 대신 컬렉터가 나중에 그 자식을 정리하면서 OwnerRefInvalidNamespace 라는 경고 이벤트를
남긴다. 이벤트는 기본 한 시간이면 사라지므로, 그 시간을 지나 조사에 들어가면 자식이 왜 없어졌는지 알
방법이 거의 없다.

삭제 전파는 세 가지다.

| --cascade | 부모 | 자식 | 언제 쓰나 |
| --- | --- | --- | --- |
| background(기본) | 즉시 사라진다 | 컬렉터가 뒤따라 지운다 | 보통의 삭제 |
| foreground | 자식이 다 사라진 뒤에 사라진다 | 먼저 지워진다 | 정리 순서가 중요할 때 |
| orphan | 즉시 사라진다 | 남고 참조만 떨어진다 | 오퍼레이터만 걷어 낼 때 |

foreground 는 대기를 표현하려고 부모에 foregroundDeletion 이라는 파이널라이저를 붙인다. 부모는
deletionTimestamp 가 찍힌 채로 남아 있고, blockOwnerDeletion: true 인 자식이 모두 사라져야 비로소
없어진다. orphan 은 자식을 지우지 않는 대신 자식에서 그 소유자 참조를 떼어 낸다. 안 떼면 부모 없는
참조가 남아 다음 정리 때 자식이 사라지기 때문이다.

파이널라이저는 삭제를 막는 장치가 아니라 늦추는 장치다. 파이널라이저가 하나라도 있으면 삭제 요청은
deletionTimestamp 만 찍고 끝난다. 오브젝트는 계속 조회되고 수정도 되지만 같은 이름으로 새로 만들 수는
없다. 컨트롤러는 이 구간에서 바깥 세계를 정리하고(클라우드 자원 해제, 백업 보관, 연결 종료), 끝나면
자기 파이널라이저를 목록에서 뺀다. 목록이 비는 순간 오브젝트가 진짜로 사라진다.

현장에서 만나는 모습

첫째, Terminating 에서 안 풀리는 오브젝트. 가장 흔한 원인은 파이널라이저를 지울 컨트롤러가 없는
것이다 — 오퍼레이터를 먼저 걷어 내고 그 뒤에 CR 을 지우면 정확히 이렇게 된다. 이때 사람들이 가장 자주
하는 일이 파이널라이저를 손으로 떼어 내는 것인데, 그건 정리를 건너뛰는 것이라 클라우드 자원이 그대로
남아 요금이 계속 나간다. 순서는 반대여야 한다 — CR 을 먼저 정리하고 오퍼레이터를 걷어 낸다.

둘째, 네임스페이스가 Terminating 에서 멈추는 일. 원인은 대개 그 안의 어떤 오브젝트가 파이널라이저를
달고 있는 것이다. 네임스페이스를 억지로 지우면 그 안의 오브젝트가 정리되지 않은 채 기록만 사라진다.

셋째, 진단 절차를 미리 만들어 둔다. 멈춘 오브젝트를 만났을 때 물어야 할 것은 셋이다 —
deletionTimestamp 가 언제 찍혔나, 어떤 파이널라이저가 남아 있나, 그 파이널라이저를 지울 주체가
지금 살아 있나. foregroundDeletion 이 남아 있다면 질문이 하나 더 붙는다 — blockOwnerDeletion
참인 자식 중 아직 안 사라진 것이 무엇인가. 이 네 가지를 한 번에 뽑는 스크립트를 미리 만들어 두면
사고 때 처음부터 생각하지 않아도 된다.

이 실습 환경의 한계

kwok 클러스터의 파드는 가짜라서 컨테이너가 실제로 실행되지 않는다. 그래서 파이널라이저 안에서
바깥 시스템을 정리하는 진짜 작업(클라우드 API 호출, 볼륨 해제)은 흉내만 낸다. 반대로 가비지
컬렉터와 삭제 전파 자체는 진짜 kube-controller-manager 가 처리하므로, uid 가 틀린 자식이 사라지는
것도, foreground 삭제가 파이널라이저를 남기고 막히는 것도 실제 동작 그대로다.

다음 실습에서 할 것

Site 라는 부모 타입을 만들고 uid 가 맞는 자식과 틀린 자식을 나란히 만들어 가비지 컬렉터가 무엇을 하는지
본다. 네임스페이스를 넘는 참조가 남기는 경고 이벤트를 확인하고, --cascade 세 가지를 각각 돌려 결과를
오브젝트로 증명한다. 마지막에는 foreground 로 멈춰 둔 오브젝트를 대상으로, 멈춘 부모와 막고 있는 자식을
한 번에 뽑아 주는 진단 스크립트를 만든다.