CRD 와 오퍼레이터 · CR 로 애플리케이션 배포하기 · 이론
CR 은 배포의 입력이자 상태 보고서다
한 줄 요약
CR 은 사용자가 원하는 것을 적는 자리(spec)와 시스템이 관찰한 것을 되돌려 주는 자리(status)를 한 오브젝트 안에 갖는다. 이 둘을 섞는 순간 배포는 추적 불가능해진다.
왜 이게 필요했나
"이 서비스 지금 잘 떠 있나요?" 라는 질문에 답하는 방법을 생각해 보자. Helm 만 쓰면 릴리스 목록을 보고, 그 릴리스가 만든 Deployment 를 찾고, 그 Deployment 의 ReplicaSet 을 찾고, 파드를 세어야 한다. 도구가 알려 주는 것은 "설치 명령이 성공했다"까지이고, 그 뒤의 상태는 사람이 조립해야 한다.
CR 은 그 조립을 오브젝트 안으로 가져온다. 사용자는 spec 에 의도를 적고, 컨트롤러는 status 에 관찰 결과를 적는다. 그러면 kubectl get webservice 한 줄이 곧 배포 상태 대시보드가 된다. 다만 이 구조가 성립하려면 규율이 필요하다.
어떻게 동작하나
규율 1 — spec 은 사용자, status 는 컨트롤러. 컨트롤러가 spec 에 값을 써넣는 순간 GitOps 저장소와 클러스터가 어긋나기 시작한다. 사용자가 커밋한 매니페스트에는 없는 값이 클러스터에만 생기고, 다음 동기화가 그것을 지운다. 그래서 status 는 반드시 별도 경로로 쓴다.
잘못됨: spec 과 status 를 한 번에 update -> status 서브리소스가 켜져 있으면 status 는 무시됨올바름: status 만 서브리소스 경로로 갱신규율 2 — 명령이 아니라 상태를 적는다. spec.restartNow: true 같은 필드는 안티패턴이다. 한 번 실행되고 나면 그 값은 무의미해지고, 누가 언제 껐는지도 추적되지 않는다. 대신 spec.version 이나 spec.paused 처럼 지속적인 의미를 갖는 값으로 표현한다. 그래야 조정 루프가 언제 다시 돌아도 같은 판단을 한다.
규율 3 — 하위 리소스에는 소유자 참조를 단다. CR 이 만든 ConfigMap·Deployment·Service 에 ownerReferences 를 붙이면 두 가지가 따라온다. 첫째, 부모가 지워질 때 가비지 컬렉터가 자식을 자동으로 정리한다. 둘째, 컨트롤러가 "내가 만든 것"만 관리하게 되어 조정 범위가 명확해진다. 여기서 중요한 함정 하나 — 소유자 참조는 이름이 아니라 uid 로 연결된다. 같은 이름의 부모를 지웠다 다시 만들면 uid 가 바뀌고, 옛 uid 를 가리키는 자식은 고아가 되어 즉시 수거된다.
규율 4 — status 는 conditions 표준을 따른다. 단순한 phase: Running 문자열보다 conditions 배열이 권장된다.
| 필드 | 의미 |
| --- | --- |
| type | 조건 이름 (Ready, Progressing, Degraded) |
| status | True / False / Unknown |
| reason | 기계가 읽는 짧은 이유 코드 |
| message | 사람이 읽는 설명 |
| lastTransitionTime | 상태가 마지막으로 바뀐 시각 |
reason 과 message 를 둘 다 두는 이유가 핵심이다. 알림 규칙은 reason 으로 분기하고, 당직자는 message 를 읽는다. 하나만 두면 둘 중 하나가 불편해진다.
규율 5 — observedGeneration 으로 시차를 드러낸다. metadata.generation 은 spec 이 바뀔 때마다 오르고, status.observedGeneration 은 컨트롤러가 마지막으로 처리한 세대다. 둘이 다르면 "아직 최신 명세를 반영하지 않았다"는 뜻이다. 이 한 쌍이 없으면 사용자는 status 가 새 spec 을 반영한 것인지 옛 spec 의 잔재인지 구분할 수 없다.
현장에서 만나는 모습
첫째, status 만 보고 판단하는 사고. status 는 컨트롤러가 관찰한 결과를 캐시한 것이지 진실의 원천이 아니다. 진짜 상태는 실제 리소스에 있다. status 만 보고 분기하는 컨트롤러는 status 가 낡았을 때 잘못된 결정을 내린다.
둘째, status 에 배열을 무한히 쌓는 사고. 이벤트나 로그를 status 배열에 누적하면 갱신할 때마다 오브젝트 전체가 다시 쓰이고, informer 캐시 메모리도 함께 부푼다. conditions 처럼 크기가 고정된 구조를 써야 한다.
셋째, 커스텀 컬럼과 라벨의 조합. CR 이 정식 오브젝트라는 사실의 가장 실용적인 결과는 라벨 셀렉터다. spec.tier 에 값을 넣는 것과 metadata.labels.tier 를 붙이는 것은 다른 일이다. 앞의 것은 컨트롤러가 읽는 의도이고, 뒤의 것은 사람과 도구가 고르는 색인이다. 둘 다 필요하다.
다음 실습에서 할 것
crd-lab 네임스페이스에 최소 명세 CR 과 전체 명세 CR 을 각각 만들어 기본값 주입을 확인하고, 소유자 참조로 자식 ConfigMap 을 부모에 묶고, status 서브리소스에 관측값과 Ready 조건을 직접 쓴다. 그다음 라벨 셀렉터와 커스텀 컬럼으로 원하는 것만 뽑아 보고, 마지막에는 CR 의 명세에서 파생된 "원하는 상태" 오브젝트를 만들어 다음 모듈의 조정 루프로 넘어갈 준비를 한다.