CRD 와 오퍼레이터 · CR 로 애플리케이션 배포하기 · 실습
CR 한 장에서 하위 리소스와 상태 만들기
목표
CR 한 장을 입력으로 삼아 하위 리소스를 만들고, 그 결과를 status 에 되돌려 적어, kubectl get 한 줄이 배포 상태를 말해 주는 구조를 손으로 완성합니다.
왜 중요한가
CR 을 배포 인터페이스로 쓸 때 지켜야 할 규율은 네 가지입니다. 첫째, spec 은 사용자가 쓰고 status 는 컨트롤러가 씁니다. 컨트롤러가 spec 을 건드리면 Git 저장소와 클러스터가 어긋나 다음 동기화가 그것을 지웁니다. 둘째, status 는 반드시 별도 경로로 씁니다. 서브리소스가 켜져 있는데 일반 update 로 쓰면 status 가 조용히 무시되기 때문에, "분명히 썼는데 반영이 안 된다"는 증상으로 나타납니다. 셋째, 자식에는 소유자 참조를 답니다. 부모가 지워질 때 가비지 컬렉터가 자식을 자동으로 정리해 주고, 컨트롤러는 "내가 만든 것"만 관리하면 되므로 정리 로직이 단순해집니다. 이때 연결 고리는 이름이 아니라 uid 입니다 — 같은 이름의 부모를 지웠다 다시 만들면 uid 가 바뀌고 옛 uid 를 가리키는 자식은 즉시 수거됩니다. 넷째, observedGeneration 으로 시차를 드러냅니다. 이 값이 metadata.generation 보다 작으면 "status 는 아직 옛 명세 기준"이라는 뜻이고, 이 한 쌍이 없으면 사용자는 status 를 믿어도 되는지 알 수 없습니다.
단계
시작 전 준비: 실습 파드는 실습마다 새로 뜨므로 앞 실습의 클러스터 상태는 남아 있지 않습니다. kubectl get crd webservices.apps.labhub.io 가 비어 있으면 CRD 를 다시 작성해 적용하고 kubectl create ns crd-lab 도 하세요. 이 실습에는 v1 의 additionalPrinterColumns(Image/Replicas/Tier/Age), subresources.status, subresources.scale(.spec.replicas/.status.replicas/.status.selector), 그리고 properties.status 아래 replicas·selector·observedGeneration·conditions 정의가 모두 필요합니다. status 스키마가 없으면 patch 해도 pruning 으로 잘려 나갑니다.
1. /root/crd/deploy/minimal.yaml 에 metadata.name: minimal(네임스페이스 crd-lab)이고 spec.image: nginx:1.27 만 있는 WebService 를 쓰고 적용하세요. spec.replicas 는 적지 마세요. 저장된 오브젝트의 spec.replicas 가 1 이어야 합니다.
2. /root/crd/deploy/storefront.yaml 에 metadata.name: storefront, metadata.labels.tier: prod, spec.image: nginx:1.27, spec.replicas: 4, spec.tier: prod 인 WebService 를 쓰고 적용하세요.
3. crd-lab 에 ConfigMap storefront-config 를 만들고 metadata.ownerReferences[0] 에 apiVersion: apps.labhub.io/v1, kind: WebService, name: storefront, controller: true, 그리고 uid 는 kubectl get webservice storefront -n crd-lab -o jsonpath='{.metadata.uid}' 로 조회한 실제 값을 넣으세요.
4. storefront 의 status 에 replicas: 4 와 selector: app=storefront 를 쓰세요. 반드시 --subresource=status 를 붙인 patch 로 써야 하며, 사용한 명령줄 전체를 /root/crd/deploy/out/status-patch.txt 에 저장하세요.
5. 같은 status 에 conditions 를 추가하세요. type: Ready, status: "True", reason: AllReplicasReady, message 는 자유 문장, lastTransitionTime 은 date -u +%Y-%m-%dT%H:%M:%SZ 형식의 RFC3339 시각입니다. 함께 status.observedGeneration 을 metadata.generation 과 같은 값으로 쓰세요.
6. /opt/lab/fixtures/crd/sample-cr.yaml 을 crd-lab 에 적용해 sample 을 추가하세요(이로써 WebService 가 3개가 됩니다). 그다음 kubectl label webservice minimal -n crd-lab tier=dev 로 라벨을 붙이고, kubectl get webservice -n crd-lab -l tier=prod 의 출력을 /root/crd/deploy/out/selected.txt 로 저장하세요. 이 파일에 storefront 는 있고 minimal 은 없어야 하며, tier=prod 로 걸리는 것은 정확히 하나여야 합니다.
7. kubectl get webservice -n crd-lab -o custom-columns=NAME:.metadata.name,IMAGE:.spec.image,TIER:.spec.tier 의 출력을 /root/crd/deploy/out/columns.txt 로 저장하세요. 머리글 한 줄에 리소스 3줄 이상, 총 4줄 이상이어야 합니다.
8. crd-lab 에 ConfigMap storefront-desired 를 만드세요. data.image, data.replicas, data.tier 세 키의 값은 storefront 의 spec 에서 읽은 값과 문자열로 정확히 같아야 하고, 3번과 같은 방식으로 storefront 를 소유자로 지정하세요. 이 단계 후에도 status.observedGeneration 은 metadata.generation 과 같아야 합니다.
참고
- 실습 파드는 실습마다 새로 뜨므로 앞 실습의 클러스터 상태는 남아 있지 않습니다. 그래도 선언을 파일로 남겨 두면 어느 파드에서든 같은 상태를 다시 세울 수 있습니다 — 이것이 선언형의 실질적 이점입니다.
- status 쓰기 예:
kubectl patch webservice storefront -n crd-lab --subresource=status --type=merge -p '{"status":{"replicas":4}}' - ConfigMap 의
data값은 항상 문자열입니다. 숫자 4 는"4"로 적어야 하며, 그렇지 않으면 적용이 거부됩니다. - 소유자 참조를 넣은 오브젝트는
kubectl apply대신 파일을 만들어 적용하는 편이 편합니다. uid 를 셸 변수로 받아 매니페스트에 끼워 넣으세요. - 흔한 실수 1: 4번에서
--subresource=status없이 patch 하는 것. 서브리소스가 켜져 있으면 status 가 조용히 무시되어 아무 오류도 안 나면서 값이 저장되지 않습니다. - 흔한 실수 2: 3번에서 uid 대신 이름만 맞추는 것. 이름이 같아도 uid 가 다르면 가비지 컬렉터는 그 자식을 고아로 보고 즉시 지웁니다.
- 흔한 실수 3: 8번에서 spec 을 다시 수정해 generation 이 오른 뒤 observedGeneration 을 갱신하지 않는 것.
단계 8개
- 최소 명세로 CR 만들기
- 전체 명세를 채운 CR 만들기
- 소유자 참조로 자식 묶기
- status 서브리소스에 관측값 쓰기
- 표준 conditions 와 observedGeneration 채우기
- 라벨 셀렉터로 CR 고르기
- custom-columns 로 원하는 필드만 뽑기
- CR 명세에서 원하는 상태 오브젝트 만들기