CRD를 마침내 이해하게 되기까지...

|Platform Decision|8분 읽기

터미널을 켜고 확인해보세요

문득 궁금해서 터미널에 이 명령어를 입력해봤습니다.

kubectl get crds

스크롤이 한참 내려갔습니다. 수백 개의 라인이 쏟아져 나오더라고요. ArgoCD Application, KEDA ScaledObject, cert-manager Certificate... 모두 익숙한 이름들이었는데, 이때 깨달은 게 있었습니다.

저는 수년간 쿠버네티스를 사용하면서도, 정작 쿠버네티스가 어떻게 확장되는지 제대로 이해하지 못하고 있었다는 것을요.

쿠버네티스는 플랫폼이 아니라 언어입니다

대부분 사람들이 쿠버네티스를 생각할 때 떠올리는 건 Pod, Deployment, Service 정도입니다. 저도 그랬고요. 하지만 이건 겉면일 뿐이더라고요.

실제로 쿠버네티스는 플랫폼이 아니라 언어에 가깝습니다. 그리고 CRD는 그 언어에 새로운 단어를 추가할 수 있게 해주는 도구죠.

생각해보면 신기한 일입니다. DatabaseCluster, MLModel, TenantConfig 같은 개념을 정의하는 순간, 이들은:

  • 일급 API 객체가 됩니다
  • kubectl로 쿼리 가능해집니다
  • etcd에 저장됩니다
  • RBAC로 보안이 적용됩니다
  • 네이티브 리소스처럼 watch 할 수 있습니다

즉, 우리의 아이디어가 쿠버네티스 자체의 일부가 되는 거죠.

실제로 CRD를 만들어보기

이론보다는 실제로 만들어보는 게 이해가 빠를 것 같아서, DatabaseCluster라는 CRD를 작성해봤습니다.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databaseclusters.infra.example.com
spec:
  group: infra.example.com
  scope: Namespaced
  names:
    plural: databaseclusters
    singular: databasecluster
    kind: DatabaseCluster
    shortNames:
      - dbc
  versions:
    - name: v1alpha1
      served: true
      storage: true
      subresources:
        status: {}
      additionalPrinterColumns:
        - name: Replicas
          type: integer
          jsonPath: .spec.replicas
        - name: Region
          type: string
          jsonPath: .spec.region
        - name: Phase
          type: string
          jsonPath: .status.phase
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: ["engine", "replicas", "region"]
              properties:
                engine:
                  type: string
                  enum: ["postgres", "mysql", "mariadb"]
                replicas:
                  type: integer
                  minimum: 1
                  maximum: 9
                region:
                  type: string
                storageGB:
                  type: integer
                  minimum: 10
                  default: 20
            status:
              type: object
              properties:
                phase:
                  type: string
                endpoint:
                  type: string

여기서 중요한 건 몇 가지입니다:

group과 names: infra.example.com/v1alpha1이라는 새로운 API 엔드포인트가 생성됩니다. 서버 재시작 없이요.

scope: Namespaced로 설정하면 네임스페이스 내에서 관리되고, Cluster로 하면 클러스터 전체에서 관리됩니다.

subresources의 status: 이게 핵심입니다. 사용자는 spec(원하는 상태)만 정의하고, 오직 컨트롤러만 status(실제 상태)를 업데이트할 수 있게 됩니다.

CRD는 아무것도 실행하지 않습니다

여기서 많은 분들이 헷갈려하는 부분이 있는데요. CRD 자체는 아무것도 실행하지 않습니다.

CRD는 단순히 의도만 정의할 뿐이에요. 이렇게 생각하면 됩니다:

  • CRD = 어휘 정의
  • Operator/Controller = 실제 동작

컨트롤러 없이는 kubectl get dbc를 해도 그냥 객체가 거기 있을 뿐, 실제로는 아무 일도 일어나지 않습니다.

# my-database.yaml
apiVersion: infra.example.com/v1alpha1
kind: DatabaseCluster
metadata:
  name: production-postgres
  namespace: databases
spec:
  engine: postgres
  replicas: 3
  region: ap-northeast-2
  storageGB: 100

이걸 적용하면:

kubectl apply -f my-database.yaml
kubectl get dbc -n databases

# NAME                 REPLICAS   REGION           PHASE     AGE
# production-postgres  3         ap-northeast-2   <none>    10s

Phase가 <none>인 이유는 아직 컨트롤러가 없기 때문입니다.

status 서브리소스의 중요성

처음에는 왜 굳이 subresourcesstatus: {}를 넣어야 하는지 이해하지 못했었는데요. 나중에 알고 보니 이게 초보자와 실제 플랫폼 엔지니어를 구분하는 포인트더라고요.

status 서브리소스가 있으면:

  • 사용자는 spec만 수정할 수 있습니다
  • 오직 컨트롤러만 status를 업데이트할 수 있습니다
  • API 레벨에서 이 분리가 강제됩니다

이건 단순한 관습이 아니라 강제된 아키텍처입니다. 사용자가 "원하는 상태"를 정의하고, 컨트롤러가 "실제 상태"를 보고하는 구조죠.

숨겨진 kubectl 트릭

additionalPrinterColumns를 추가하면 kubectl 출력이 완전히 달라집니다:

kubectl get dbc

# NAME                 REPLICAS   REGION           PHASE      AGE
# production-postgres  3         ap-northeast-2   Ready      2d
# staging-mysql       1         ap-northeast-2   Pending    5m

UI 없이도 kubectl이 대시보드가 되는 거죠.

실무에서 피해야 할 실수들

몇 년간 CRD를 다루면서 자주 보는 실수들이 있습니다:

❌ 스키마 없이 CRD 만들기: 아무 값이나 들어가서 나중에 디버깅이 지옥이 됩니다.

❌ status 서브리소스 빼먹기: 사용자가 시스템 상태를 덮어쓸 수 있게 됩니다.

❌ 무작정 스키마 변경: 기존 리소스들이 깨집니다.

❌ 컨트롤러 로직 없이 CRD만 배포: 아무 일도 일어나지 않습니다.

대부분의 CRD 문제는 쿠버네티스 문제가 아니라 API 설계 문제더라고요.

마침내 이해하게 된 순간

CRD를 이해하고 나니 클라우드 네이티브 생태계 전체가 다르게 보이기 시작했습니다. ArgoCD, KEDA, cert-manager... 이들이 모두 같은 원리로 동작한다는 걸 깨달았거든요.

이들은 쿠버네티스를 "확장"하는 게 아니라, 쿠버네티스 언어에 새로운 어휘를 추가하고 있었던 겁니다. 플러그인도 해킹도 아닌, 그냥 새로운 명사들이었던 거죠.

그제서야 왜 쿠버네티스가 이렇게 강력한 생태계를 가지고 있는지 이해하게 됐습니다. API를 확장할 수 있는 표준화된 방법이 있으니까, 모든 도구들이 일관된 방식으로 동작할 수 있었던 거더라고요.

수년간 매일 사용하던 도구의 진짜 모습을 이제야 알게 된 기분이었습니다.

#kubernetes#CRD#CustomResourceDefinition#인프라#데브옵스