LabHub

CNPA — 클라우드 네이티브 플랫폼 엔지니어링 어소시에이트 · 플랫폼 API 와 추상화 · 이론

쿠버네티스 API 는 왜 플랫폼의 공용어가 됐나

LabHub 에서 이어서 보기

한 줄 요약

쿠버네티스가 이긴 것은 컨테이너 오케스트레이션 경쟁이 아니라 API 규격 경쟁 이었습니다. 선언적 리소스 + 조정 컨트롤러라는 한 쌍의 패턴이 컨테이너 바깥까지 퍼졌고, 그래서 플랫폼 API 를 새로 만들 때 CRD 가 기본 선택지가 됐습니다.

왜 이게 필요했나

플랫폼 팀이 "서비스 하나 띄우기"를 쉽게 만들려고 하면 보통 이런 길을 밟습니다.

1. 위키에 절차를 적는다 → 아무도 최신으로 유지하지 않는다.
2. 셸 스크립트를 만든다 → 실행 환경마다 결과가 다르고, 실패하면 중간 상태가 남는다.
3. 사내 웹 앱을 만든다 → 상태 저장, 인증, 감사, 재시도, 동시성을 전부 직접 구현해야 한다. 그리고 그 앱이 새 SPOF 가 된다.

3 번을 끝까지 만들어 보면 결국 무엇을 다시 만들고 있는지 깨닫습니다. 상태 저장소, 낙관적 동시성 제어, 인증·인가, 감사 로그, 감시(watch), 그리고 조정 루프 — 전부 쿠버네티스 API 서버가 이미 가진 것들입니다.

그래서 방향이 뒤집힙니다. 플랫폼 API 를 새로 만들지 말고 쿠버네티스 API 를 확장 합니다. CRD 를 등록하는 순간 공짜로 따라오는 것이 이만큼입니다.

이게 "쿠버네티스 API 가 플랫폼의 공용어"라는 말의 실질입니다. 새 어휘(CRD)를 정의하되 문법(API 규약)은 모두가 이미 아는 것을 쓰는 셈입니다.

어떻게 동작하나

CRD + 컨트롤러 = 플랫폼 API

두 조각이 필요합니다.

CRD 는 어휘를 정의합니다. group, version, kind, 스코프(Namespaced 인가 Cluster 인가), 그리고 OpenAPI v3 스키마입니다. 스키마가 하는 일이 생각보다 큽니다.

spec:  versions:    - name: v1alpha1      served: true      storage: true      schema:        openAPIV3Schema:          type: object          properties:            spec:              type: object              required: [image]              properties:                image:    { type: string }                replicas: { type: integer, default: 2, minimum: 1, maximum: 10 }                public:   { type: boolean, default: false }

컨트롤러 는 그 어휘에 의미를 부여합니다. WebService 라는 CR 을 보고 Deployment, Service, HPA, NetworkPolicy 를 만들어 주는 조정 루프입니다. CRD 만 있고 컨트롤러가 없으면 그 CR 은 "구조가 검증되는 설정 파일"에 머뭅니다 — 그것도 나름 쓸모가 있지만 플랫폼 API 는 아닙니다.

스코프 선택도 시험 단골입니다. Namespaced 는 테넌트 경계 안에 갇히고 네임스페이스 RBAC 이 그대로 먹습니다. Cluster 스코프는 이름이 전역이라 테넌트끼리 이름이 충돌하고, Role 이 아니라 ClusterRole 로만 권한을 줄 수 있습니다. 테넌트가 만드는 리소스는 거의 항상 Namespaced 여야 합니다.

추상화가 새는 순간

좋은 플랫폼 API 는 "필요한 것만 묻습니다". WebService 는 이미지와 필요하면 replicas 정도만 묻고, 나머지 — 라벨 규약, 보안 컨텍스트, 리소스 기본값, 관측 애너테이션, 네트워크 정책 — 는 컨트롤러가 채웁니다.

그런데 반드시 새는 날이 옵니다.

이때 "그건 지원하지 않습니다"라고 답하면 그 팀은 플랫폼을 버리고 생 YAML 로 돌아갑니다. 한 번 나가면 돌아오지 않습니다. 그래서 탈출구(escape hatch) 를 설계에 미리 넣어야 합니다.

| 탈출구 | 형태 | 위험 |
| --- | --- | --- |
| 부분 오버라이드 | spec.podOverrides 같은 자유 필드 | 아무거나 넣으면 추상화가 무의미해짐 |
| 확장 지점 | extraEnv, extraVolumes, nodeSelector 만 허용 | 목록 관리 비용 |
| 렌더 후 이탈 | 생성된 매니페스트를 복사해 직접 관리 | 이후 플랫폼 개선을 못 받음 |

균형점은 이렇습니다. 탈출구는 있어야 하지만, 탈출구를 쓴 것이 눈에 보여야 합니다. 오버라이드를 쓴 서비스에 라벨이나 상태 조건을 남기면 플랫폼 팀이 "이 기능은 다섯 팀이 오버라이드로 우회 중"이라는 신호를 받고 정식 기능으로 승격할 수 있습니다. 이게 플랫폼을 제품으로 굴리는 피드백 루프입니다.

현장에서 만나는 모습

저자의 홈랩에서 이 문제가 정확한 형태로 등장했습니다. GPU Feature Discovery 가 노드에 카드 정보를 자동으로 라벨링합니다 — RTX 3090(24576MB, ampere), 5090(32607MB, blackwell), 4070 Laptop(8188MB, ada-lovelace) 두 장. 그런데 파드가 nvidia.com/gpu: 1 만 요청하면 32GB 가 필요한 학습이 8GB 노트북 GPU 에 얹힐 수 있습니다. 쿠버네티스에게는 둘 다 "GPU 한 개"이기 때문입니다.

GFD 가 붙인 gpu.memory 라벨은 문자열이라 "24GB 이상" 같은 비교 셀렉터가 불가능합니다. 그래서 의미 기반 라벨을 직접 얹었습니다 — gpu.homelab/tier=xlarge|large|small, gpu.homelab/vram=32g|24g|8g. 이제 워크로드는 nodeSelector: gpu.homelab/tier: xlarge 로 자기 체급을 고릅니다.

이 한 줄이 플랫폼 API 설계의 전형입니다. 하부의 물리적 사실(카드 모델명, 메모리 바이트)을 그대로 노출하지 않고 사용자가 결정을 내릴 수 있는 어휘(tier)로 번역했습니다. 동시에 탈출구도 남아 있습니다 — 정말 특정 카드가 필요하면 GFD 원본 라벨로 직접 셀렉트할 수 있습니다. 좋은 추상화는 아래 계층을 가리는 게 아니라 덮되 열어 두는 것입니다.

다음 실습에서 할 것

CRD webservices.platform.labhub.io 를 실제 클러스터에 만들고, 스키마 위반이 즉시 거절되는지와 기본값이 서버에서 채워지는지를 눈으로 확인합니다. 이어서 네임스페이스 + ResourceQuota + LimitRange 로 테넌트 경계를 긋고, RBAC 으로 셀프서비스 권한을 준 뒤, 다른 테넌트에는 손댈 수 없다는 것을 kubectl auth can-i 로 증명합니다.