Helm 차트 제작과 배포 · 클러스터 능력과 CRD · 이론
Capabilities 는 어디서 오는가, 그리고 crds 는 왜 릴리스 밖에 사는가
한 줄 요약
.Capabilities 의 값은 명령마다 출처가 다르고, crds/ 는 템플릿도 릴리스 매니페스트도 아닌 별도의 생애를 가진다.
왜 이게 필요했나
차트 하나로 여러 클러스터를 지원하기 시작하면 곧 이런 코드가 들어온다.
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}apiVersion: policy/v1{{- else }}apiVersion: policy/v1beta1{{- end }}옛 클러스터에는 policy/v1 이 없으니 베타 버전을 쓴다는 분기다. 잘 도는 것처럼 보이는데, CI 에서 helm template 으로 렌더 검사를 돌리면 언제나 else 쪽만 나온다. 반면 실제 배포에서는 if 쪽이 나온다. 같은 차트, 같은 값인데 결과가 다르다.
이유는 능력 정보의 출처가 다르기 때문이다.
| 명령 | 클러스터에 묻는가 | KubeVersion |
| --- | --- | --- |
| helm template | 아니오 | 헬름에 박힌 기본값 |
| helm template --validate | 예 | 실제 클러스터 |
| helm install --dry-run | 예 | 실제 클러스터 |
| helm install --dry-run=server | 예 | 실제 클러스터 |
| helm install | 예 | 실제 클러스터 |
helm template 만 오프라인이다. 그리고 그 기본 능력 목록은 매우 작아서, 실제 클러스터라면 당연히 있는 networking.k8s.io/v1/Ingress 조차 없다고 나온다. 이것을 모르면 "렌더해 보니 Ingress 분기가 안 타네" 하고 차트를 고치기 시작한다.
클러스터 없이 분기를 시험하려면 헬름에게 거짓말을 시키면 된다. --kube-version 1.21.0 은 버전을, --api-versions "demo.labhub.io/v1/Widget" 은 API 목록을 덧붙인다. 덧붙인다는 점이 중요하다 — 기본 목록을 대체하는 것이 아니라 더한다.
crds 디렉터리는 다섯 가지가 다르다
CRD 는 닭과 달걀 문제를 가진다. CRD 가 정의하는 사용자 자원을 같은 차트가 함께 만들려면, CRD 가 먼저 클러스터에 들어가 있어야 한다. 헬름은 이것을 crds/ 라는 전용 디렉터리로 푼다. 그 디렉터리는 보통의 템플릿과 다섯 가지가 다르다.
1. 템플릿 엔진을 거치지 않는다. 중괄호를 써도 그냥 글자다. 그래서 CRD 를 값으로 조건 분기할 수 없다.
2. 기본 렌더 결과에 나오지 않는다. helm template --include-crds 를 줘야 나온다.
3. 설치할 때 다른 모든 것보다 먼저 들어간다. 그래서 같은 차트의 템플릿이 그 CRD 를 쓰는 오브젝트를 함께 만들 수 있고, 첫 설치에서도 .Capabilities.APIVersions.Has 가 참이 된다.
4. 업그레이드에서 손대지 않는다. 차트의 crds/ 를 고치고 helm upgrade 를 돌려도 클러스터의 CRD 는 그대로다. 공식 문서가 이것을 제한으로 명시하고, CRD 갱신은 사람이 직접 하라고 안내한다.
5. 릴리스를 지워도 남는다. CRD 를 지우면 그것으로 만든 모든 사용자 자원이 함께 사라지기 때문에, 헬름은 이 결정을 사람에게 넘긴다.
helm get manifest 로 릴리스 매니페스트를 꺼내 보면 CRD 가 없다. 릴리스가 CRD 를 소유하지 않는다는 뜻이고, 위 네 번째와 다섯 번째가 여기서 따라 나온다.
대안 — 템플릿에 CRD 를 두는 방식
crds/ 의 제약(업그레이드에서 갱신되지 않음, 조건 분기 불가)이 곤란하면 CRD 를 templates/ 에 두는 선택지가 있다. 그러면 보통 오브젝트가 되어 업그레이드로 갱신되고 조건도 걸 수 있다. 대신 릴리스가 CRD 를 소유하게 되어 릴리스를 지우면 CRD 와 그 자원이 전부 사라진다. 그리고 여러 릴리스가 같은 CRD 를 쓰면 소유권이 겹쳐 충돌한다.
실무에서 자주 쓰이는 경계는 이렇다. CRD 를 연산자(operator) 차트와 분리한 별도 차트로 만들어 클러스터 관리자가 한 번만 설치하고, 애플리케이션 차트는 .Capabilities.APIVersions.Has 로 존재를 확인만 한다. 대형 프로젝트가 <이름>-crds 차트를 따로 내놓는 이유가 이것이다.
현장에서 만나는 모습
가장 많이 나는 사고는 "CRD 를 고치고 업그레이드했는데 새 필드가 안 먹는다" 는 것이다. helm history 에는 리비전이 쌓이고 배포는 성공으로 표시되는데, 클러스터의 CRD 는 옛 스키마 그대로다. 새 필드를 쓴 사용자 자원은 스키마에 없는 필드로 취급되어 조용히 잘려 나간다. 이 조합이 특히 나쁜 것은 아무 곳에서도 실패가 보고되지 않는다는 점이다. 배포 파이프라인에 CRD 를 따로 kubectl apply 하는 단계를 두거나, CRD 전용 차트를 따로 두는 것이 정석적인 대응이다.
두 번째로 흔한 것은 CI 의 렌더 검사가 운영과 다른 결과를 내는 것이다. helm template 만 돌리면 능력 분기가 언제나 한쪽으로 고정된다. CI 에서 실제 클러스터를 쓸 수 있다면 --validate 나 --dry-run=server 를 쓰고, 쓸 수 없다면 --kube-version 과 --api-versions 로 대상 클러스터를 흉내 내어 두 경우를 모두 렌더해 보는 편이 안전하다.
다음 실습에서 할 것
능력을 그대로 내보내는 차트를 만들어 오프라인 렌더의 값을 확인하고, --kube-version 과 --api-versions 로 다른 클러스터인 척 렌더한다. 능력에 따라 사용자 자원을 넣고 빼는 분기를 만들고, crds/ 에 CRD 를 둔 뒤 실제 kwok 클러스터에 설치해 매니페스트에 CRD 가 없다는 것을 확인한다. 마지막으로 CRD 를 고쳐 업그레이드해도 클러스터가 그대로인 것을 직접 보고, --validate 와 --dry-run=server 가 오프라인 렌더와 어떻게 갈리는지 정리한다.