포스트

CloudNativePG 내용 정리 — Part 1: CNPG 오퍼레이터

오퍼레이터 설치 직후 클러스터에 등록되는 CRD 11개와 admission webhook, 컨트롤러 프로세스, api-resources 를 명령별로 정리했습니다.

CloudNativePG 내용 정리 — Part 1: CNPG 오퍼레이터

아래 출력은 kind 4노드 클러스터(Kubernetes 1.34)에 CloudNativePG 오퍼레이터 1.30.0 을 설치한 직후 실측한 것입니다.
모든 명령에 --context kind-cnpg-lab 을 명시했습니다.

들어가며

CloudNativePG 오퍼레이터를 설치하면 CRD와 admission webhook 설정, 컨트롤러 Deployment가 한꺼번에 클러스터에 들어옵니다. 무엇이 들어왔는지는 문서가 아니라 클러스터에 직접 물어서 확인할 수 있고, 명령 네 개면 전부 드러납니다.

  1. 어떤 리소스 종류가 등록됐는가 (get crd)
  2. 그것을 지키는 프로세스는 무엇인가 (get deploy -n cnpg-system)
  3. 그 프로세스가 kubectl apply 에 어떻게 끼어드는가 (get validatingwebhookconfiguration)
  4. 등록된 것이 실제로 서빙되고 있는가 (api-resources)

네 갈래를 순서대로 정리합니다.

등록된 CRD 11개

CRD(Custom Resource Definition) 는 쿠버네티스 기본 API를 확장해서, PostgreSQL 클러스터와 그 부속 요소를 YAML 형태의 선언적 객체로 다룰 수 있게 해 주는 사용자 정의 리소스 명세입니다. “이 클러스터에서는 Cluster 라는 종류의 오브젝트를 만들 수 있다” 를 API 서버에 등록하는 행위입니다.

오퍼레이터는 이렇게 등록된 리소스를 계속 감시하면서, 사용자가 명시한 원하는 상태(desired state) 와 실제 상태의 차이를 메웁니다. 기존 PostgreSQL 운영에서 “무엇을 할지” 는 DBA의 절차였지만, CNPG에서는 선언은 DBA가 하고 실행은 오퍼레이터가 합니다.

실행한 명령

1
2
3
4
5
6
7
8
9
10
11
# 등록된 CRD 목록
kubectl --context kind-cnpg-lab get crd | grep cnpg.io

# 각 CRD가 스스로 밝히는 정의. 문서가 아니라 API 서버에 박힌 1차 출처입니다
kubectl --context kind-cnpg-lab explain failoverquorums | sed -n '/DESCRIPTION/,/FIELDS/p'

# 필드 구조로 성격을 판별합니다. spec이 있나, status만 있나
kubectl --context kind-cnpg-lab explain failoverquorums.status
kubectl --context kind-cnpg-lab explain databaseroles.spec
kubectl --context kind-cnpg-lab explain databases.spec
kubectl --context kind-cnpg-lab explain databases.status

출력 읽는 법

목록은 두 열입니다. 한 줄만 떼어 보면 이렇게 읽힙니다.

1
2
3
backups.postgresql.cnpg.io                2026-08-05T01:26:54Z
└──┬──┘└────────┬────────┘                └────────┬─────────┘
복수형      API 그룹                         CRD 등록 시각

두 번째 열은 CRD가 등록된 시각이지 백업이 수행된 시각이 아닙니다. 11개가 전부 같은 초에 찍힌 것은 하나의 매니페스트에서 동시에 들어왔다는 뜻입니다.

역할군으로 묶은 11개

클러스터 본체

Cluster 가 가장 핵심입니다. PostgreSQL 버전, 인스턴스 수(primary와 replica), 스토리지 크기, 리소스 제한, 복제 방식까지 데이터베이스 클러스터 전체 형상을 이 하나에 정의합니다. 이 시리즈에서 여러 편에 걸쳐 층층이 벗겨낼 대상이 사실상 이 CR 하나입니다.

백업과 복구

Backup 은 일회성 온디맨드 백업 요청 한 건이 오브젝트 한 개에 대응합니다. 특징적인 건 백업이 끝나도 오브젝트가 사라지지 않고 backupId, 시작과 종료 시각, phase 를 담은 채로 남는다는 점입니다.

오브젝트 목록 자체가 백업 이력 테이블 역할 을 합니다. 별도 카탈로그를 조회할 필요 없이 kubectl get backups 가 곧 이력 조회입니다.

ScheduledBackup 은 Cron 식으로 백업 일정을 예약합니다. 주의할 구조가 하나 있는데, 이 CR이 직접 백업을 수행하는 게 아니라 시간이 되면 Backup 오브젝트를 낳습니다. 그래서 “스케줄이 실제로 돌았는가” 는 ScheduledBackup을 봐서는 알 수 없고, 파생된 Backup 오브젝트가 생겼는지로 판정합니다.

접속 경로

Pooler 는 PgBouncer 기반 커넥션 풀러를 배포하고 연결 정보를 관리합니다. 읽기 쓰기용(rw)과 읽기 전용(ro)을 각각 별도 오브젝트로 만듭니다.

데이터베이스 내부 객체의 선언적 관리

이 그룹이 CNPG가 최근 가장 빠르게 확장하는 영역입니다. PostgreSQL 내부 객체인 데이터베이스, 롤, 논리복제까지 쿠버네티스 오브젝트로 끌어올린 것입니다.

Database 는 데이터베이스를 선언적으로 관리합니다. encoding, builtinLocale, connectionLimit 같은 생성 옵션은 물론이고, 눈여겨볼 필드가 databaseReclaimPolicy 입니다. deleteretain 중 하나를 고르는데, CR을 지웠을 때 실제 데이터베이스도 지울지 를 정합니다. PersistentVolume의 reclaim policy와 같은 발상입니다.

그리고 .statusextensions, fdws, schemas, servers 가 있어서 확장과 FDW, 스키마까지 이 CR의 관장 범위입니다.

DatabaseRole 은 1.30에서 새로 들어온 CRD로, 롤을 선언적으로 관리합니다. bypassrls, createdb, createrole, connectionLimit 같은 롤 속성이 그대로 필드가 되어 있습니다.

특히 clientCertificate 필드가 있어서, 롤을 만들면서 동시에 클러스터의 client CA로 서명한 TLS 클라이언트 인증서를 발급하고 그것을 <DatabaseRole 이름>-client-cert 라는 이름의 Secret에 넣습니다. 롤 생성과 인증서 발급이 한 선언으로 묶인 셈 입니다.

PublicationSubscription 은 논리복제의 양쪽 끝을 각각 선언합니다.

이미지 관리

ImageCatalogClusterImageCatalog 는 사용할 PostgreSQL 이미지를 버전별로 그룹화해 관리하는 카탈로그입니다. 둘의 차이는 적용 범위인데, ImageCatalog는 네임스페이스 범위이고 ClusterImageCatalog는 클러스터 전역 입니다. 11개 중 클러스터 전역인 것은 이 하나뿐입니다. 플랫폼팀이 전역 카탈로그를 깔아 두고 각 팀 네임스페이스가 그것을 참조하는 운영 모델을 상정한 설계입니다.

오퍼레이터 내부 상태

FailoverQuorum 하나만 성격이 다릅니다. 나머지 10개는 DBA가 작성하는 선언이지만, 이것은 시스템이 쓰고 시스템이 읽는 오브젝트입니다. API 서버에 박힌 설명이 이렇습니다.

FailoverQuorum contains the information about the current failover quorum status of a PG cluster. It is updated by the instance manager of the primary node and reset to zero by the operator to trigger an update.

그리고 spec 이 아예 없고 status 만 있습니다. kubectl explain failoverquorums.specfield "spec" does not exist 로 거절당합니다. 안에 든 필드는 method, primary, standbyNames, standbyNumber 네 개입니다.

왜 굳이 별도 오브젝트인지가 여기서 풀립니다. 보통 이런 상태값은 Cluster.status 에 넣지만, 작성 주체가 오퍼레이터가 아니라 primary Pod 안의 instance manager 입니다. instance manager는 각 PostgreSQL Pod 안에서 도는 프로세스이고, 오퍼레이터와는 권한이 다른 별개의 주체입니다. instance manager에게 Cluster 전체에 대한 쓰기 권한을 주지 않으면서 “현재 동기 복제가 실제로 몇 대와 맺어져 있는가” 만 보고하게 하려면 오브젝트를 분리하는 수밖에 없습니다. RBAC 권한 경계가 데이터 모델을 결정한 사례 입니다.

reset to zero ... to trigger an update 라는 표현도 눈여겨볼 만합니다. 오퍼레이터가 값을 0으로 지워서 “다시 보고하라” 는 신호를 보냅니다. 폴링이 아니라 오브젝트를 매개로 한 단방향 요청과 응답 채널입니다.

오퍼레이터 본체

등록된 CR을 실제로 지키는 쪽이 오퍼레이터입니다.

오퍼레이터 는 CR에 선언된 원하는 상태와 실제 클러스터 상태를 계속 비교하며 그 차이를 메우는 조정 루프(reconcile loop) 프로그램입니다. 쿠버네티스에서는 이것도 그냥 하나의 Deployment로 배포됩니다.

CNPG에서 특히 중요한 것은 제어 평면과 데이터 평면이 분리되어 있다 는 점입니다.

구성 요소어디서 도나무엇을 하나
오퍼레이터(cnpg-controller-manager)별도 Deployment승격 결정, Pod와 PVC 생성, 롤링 업데이트, webhook 응답
instance manager각 PostgreSQL Pod 안postgresql.conf 렌더링, PG 프로세스 기동과 감시, 상태 보고

이 분리 때문에 오퍼레이터가 죽어도 이미 떠 있는 PostgreSQL은 계속 서비스합니다. 멈추는 것은 조정 작업, 그러니까 승격이나 롤링, Pod 재생성이지 데이터 경로가 아닙니다. 장애 대응 시 이 구분이 판단을 크게 바꿉니다.

실행한 명령

1
2
3
4
5
6
7
8
kubectl --context kind-cnpg-lab get deploy,pod -n cnpg-system -o wide

# 이 프로세스가 실제로 무엇을 하는지는 실행 인자에 드러납니다
kubectl --context kind-cnpg-lab get deploy cnpg-controller-manager -n cnpg-system \
  -o jsonpath='{.spec.template.spec.containers[0].command}{"\n"}{.spec.template.spec.containers[0].args}{"\n"}{range .spec.template.spec.containers[0].ports[*]}{.name}={.containerPort} {end}{"\n"}'

# webhook 서비스가 이 Pod를 가리키는지 확인합니다
kubectl --context kind-cnpg-lab get svc -n cnpg-system

Deployment 열 읽기

1
2
NAME                      READY  UP-TO-DATE  AVAILABLE  AGE  CONTAINERS  IMAGES                                        SELECTOR
cnpg-controller-manager   1/1    1           1          61m  manager     ghcr.io/cloudnative-pg/cloudnative-pg:1.30.0  app.kubernetes.io/name=cloudnative-pg
의미
READY 1/1준비된 Pod 수 / 목표 replica 수
UP-TO-DATE 1현재 spec을 반영한 replica 수. 롤아웃 중에는 이 값이 먼저 벌어집니다
AVAILABLE 1minReadySeconds 를 넘겨 실제로 트래픽을 받을 수 있는 수
CONTAINERS manager컨테이너 하나. 사이드카가 없습니다
SELECTORcnpg-webhook-service 도 같은 셀렉터를 씁니다

마지막 줄이 중요합니다. webhook 서비스가 이 Deployment와 같은 셀렉터를 쓴다는 것은 webhook 서버와 조정 루프가 같은 프로세스 안에 있다 는 뜻입니다.

Pod 열 읽기

1
2
NAME                                       READY  STATUS   RESTARTS  AGE  IP          NODE             NOMINATED NODE  READINESS GATES
cnpg-controller-manager-68bfd8895b-c5mn8   1/1    Running  0         61m  10.244.2.3  cnpg-lab-worker  <none>          <none>
조각의미
68bfd8895bReplicaSet의 pod-template-hash. 이미지나 spec을 바꾸면 이 해시가 바뀌므로, 롤아웃이 실제로 돌았는지 확인하는 가장 빠른 신호입니다
c5mn8Pod 이름의 랜덤 접미사
RESTARTS 0이 값이 오르기 시작하면 OOM이나 크래시 루프를 의심합니다
10.244.2.3kindnet CNI 대역. 10.244.<노드 인덱스>.x 구조라 IP만 보고 어느 노드인지 알 수 있습니다
cnpg-lab-workercontrol-plane이 아닌 worker에 배치됐습니다. kind의 control-plane taint 때문입니다
NOMINATED NODE <none>preemption으로 자리를 비워 주기로 예약된 노드가 없습니다
READINESS GATES <none>컨테이너 probe 외에 추가 readiness 조건이 없습니다

실행 인자가 알려 주는 것

1
2
3
4
5
6
7
8
/manager controller \
  --leader-elect \
  --max-concurrent-reconciles=10 \
  --config-map-name=cnpg-controller-manager-config \
  --secret-name=cnpg-controller-manager-config \
  --webhook-port=9443

포트: metrics=8080, webhook-server=9443
  • --leader-elect 는 replica가 1인데도 리더 선출을 켭니다. 업그레이드 중에 구버전 Pod와 신버전 Pod가 잠시 공존할 때, 둘이 동시에 조정을 수행하는 것을 막기 위한 장치입니다.
  • --max-concurrent-reconciles=10 은 동시에 조정할 수 있는 오브젝트 수입니다. 관리하는 클러스터가 수십 개인 환경에서는 튜닝 대상이 됩니다.
  • --config-map-name--secret-name 은 오퍼레이터 자신의 전역 설정이 어디서 오는지를 가리킵니다. 둘 다 cnpg-controller-manager-config 를 보고 있습니다.
  • --webhook-port=9443 은 webhook 서버가 이 프로세스 안에 있다는 사실을 확정합니다.
  • metrics=8080 은 Prometheus 스크레이프 대상입니다.

replica가 1이라 오퍼레이터 자체는 HA 구성이 아닙니다. 그래도 데이터 평면이 분리되어 있어 서비스 영향은 없고, 이것이 CNPG가 replica 1을 기본으로 두는 근거입니다.

Admission webhook

Admission webhook 은 API 서버가 오브젝트를 etcd에 저장하기 직전에 외부 서비스에게 물어보는 확장 지점입니다. 여기서 외부 서비스는 CNPG 오퍼레이터입니다. 두 종류가 있습니다.

  • Mutating(변형) webhook 은 오브젝트를 고쳐서 돌려줍니다. 사용자가 쓰지 않은 필드에 기본값을 채워 넣는 역할입니다.
  • Validating(검증) webhook 은 고치지 않고 통과와 거부만 판정합니다.

CRD에도 스키마 검증이 있는데 webhook이 또 필요한 이유는, 스키마만으로는 표현할 수 없는 규칙이 있기 때문입니다. “인스턴스 수와 동기 복제 설정의 관계가 성립하는가” 나 “지정한 이미지 태그의 PostgreSQL 버전이 현재 클러스터보다 낮지 않은가” 같은 것은 필드 하나만 봐서는 판정할 수 없고 오브젝트 전체 맥락이 필요합니다.

실행한 명령

1
2
3
4
5
6
7
8
9
10
11
12
13
# 개요
kubectl --context kind-cnpg-lab get mutatingwebhookconfiguration,validatingwebhookconfiguration | grep cnpg

# 어느 리소스에 걸려 있는지 확인합니다. 개수만 봐서는 알 수 없습니다
kubectl --context kind-cnpg-lab get mutatingwebhookconfiguration cnpg-mutating-webhook-configuration \
  -o json | jq -r '.webhooks[] | "\(.name)\n   \(.rules[] | "\(.operations|join(","))  \(.resources|join(","))")"'

kubectl --context kind-cnpg-lab get validatingwebhookconfiguration cnpg-validating-webhook-configuration \
  -o json | jq -r '.webhooks[] | "\(.name)\n   \(.rules[] | "\(.operations|join(","))  \(.resources|join(","))")"'

# 오퍼레이터 장애 시 apply가 막히는지를 결정하는 필드
kubectl --context kind-cnpg-lab get validatingwebhookconfiguration cnpg-validating-webhook-configuration \
  -o jsonpath='{range .webhooks[*]}{.name}{"  ->  "}{.clientConfig.service.namespace}/{.clientConfig.service.name}{"  failurePolicy="}{.failurePolicy}{"\n"}{end}'

출력 읽는 법

1
2
3
NAME                                     WEBHOOKS  AGE
cnpg-mutating-webhook-configuration      4         61m
cnpg-validating-webhook-configuration    5         61m

여기서 4와 5는 CRD 개수가 아니라 그 설정 오브젝트 안에 정의된 webhook 항목의 수 입니다. CNPG는 리소스 종류마다 항목을 하나씩 두고, 그것들을 configuration 오브젝트 하나에 묶어 배포합니다.

어느 리소스에 걸려 있는지를 뽑으면 이렇습니다.

대상 리소스Mutating (4)Validating (5)
clustersmcluster.cnpg.iovcluster.cnpg.io
backupsmbackup.cnpg.iovbackup.cnpg.io
scheduledbackupsmscheduledbackup.cnpg.iovscheduledbackup.cnpg.io
databasesmdatabase.cnpg.iovdatabase.cnpg.io
poolers없음vpooler.cnpg.io

전부 CREATEUPDATE 에만 걸려 있고 DELETE 에는 걸려 있지 않습니다.

요청이 지나가는 경로

kubectl apply 한 번이 etcd에 닿기까지 거치는 단계는 이렇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kubectl apply
     │
     ▼
  인증 → 인가
     │
     ▼
  Mutating admission           ← 기본값이 채워지는 곳 (오퍼레이터가 응답)
     │
     ▼
  스키마 검증 (OpenAPI + CEL)  ← CRD에 박힌 규칙, API 서버가 직접 평가
     │
     ▼
  Validating admission         ← 최종 거부 판정 (오퍼레이터가 응답)
     │
     ▼
  etcd 저장

중간의 CEL(Common Expression Language) 은 CRD 스키마 안에 직접 써 넣는 검증 표현식입니다. DatabaseRole에는 “롤 이름은 변경할 수 없다”, “postgres 는 예약어라 쓸 수 없다”, “pg_ 로 시작하는 이름은 PostgreSQL 예약이다” 같은 규칙이 CEL로 박혀 있습니다. 이것은 API 서버가 직접 평가하며 외부에 물어보지 않습니다.

mcluster.cnpg.io 가 CNPG의 자동 결정값이 실제로 주입되는 지점입니다. Cluster를 최소 형태로 작성해서 apply 한 뒤 -o yaml 로 다시 뜨면 쓰지 않은 필드가 채워져 있는데, 그 대부분이 여기를 통과하며 붙은 것입니다.

반면 SHOW 로만 보이는 PostgreSQL 파라미터는 webhook이 아니라 instance manager가 postgresql.conf 를 렌더링할 때 붙습니다. 출처가 다르면 바꾸는 방법도 다릅니다. 기본값이 어디서 왔는지를 따질 때 이 두 출처를 구분해야 하는 이유입니다.

Pooler에 mutating이 없는 것도 같은 맥락입니다. Pooler의 기본값은 admission 단계가 아니라 컨트롤러가 조정하면서 채웁니다. “선언한 적 없는 pgbouncer 파라미터가 어디서 왔나” 를 추적할 때 webhook 로그를 봐도 나오지 않습니다.

failurePolicy=Fail이 만드는 운영 제약

failurePolicy 는 API 서버가 webhook 서비스에 연결하지 못했을 때 어떻게 할지를 정합니다. Ignore 면 그냥 통과시키고, Fail 이면 요청을 거부합니다.

5개 validating webhook이 전부 Fail 이고, 대상 서비스는 cnpg-system/cnpg-webhook-service 입니다. 그리고 그 서비스가 가리키는 Pod는 오퍼레이터 자신입니다. 앞에서 --webhook-port=9443 으로 확인한 그 프로세스입니다.

오퍼레이터 Pod가 죽으면 Cluster, Pooler, Database, Backup, ScheduledBackup 의 생성과 수정이 전부 거부됩니다.

반면 DatabaseRole, Publication, Subscription, ImageCatalog 는 webhook이 없어 apply 자체는 통과합니다. 반영은 오퍼레이터가 살아나야 됩니다.

11개 CRD 중 webhook이 걸린 것은 5개뿐이고, 나머지는 앞서 본 CEL 규칙으로만 검증됩니다. CEL은 API 서버가 직접 평가하므로 오퍼레이터의 생사와 무관 합니다.

즉 CNPG는 검증 책임을 오퍼레이터에서 API 서버 쪽으로 옮겨 가는 중이고, 신규 CRD일수록 그렇습니다. 운영자 입장에서는 오퍼레이터 업그레이드 창에서 “어떤 CR은 고칠 수 있고 어떤 건 막히는지” 가 리소스마다 갈린다는 뜻입니다.

api-resources 대조

api-resources 는 API 서버가 현재 실제로 서빙하고 있는 리소스 종류의 목록입니다. 앞의 get crd 와 비슷해 보이지만 층이 다릅니다.

  • get crd 는 CRD 오브젝트가 등록되어 있다
  • api-resources 는 그 리소스를 API 서버가 실제로 제공하고 있다

등록은 됐는데 서빙이 안 되는 상태가 존재할 수 있고, 그때 두 목록의 개수가 어긋납니다.

1
kubectl --context kind-cnpg-lab api-resources --api-group=postgresql.cnpg.io
1
2
3
4
5
6
7
8
9
10
11
12
NAME                   SHORTNAMES   APIVERSION              NAMESPACED   KIND
backups                             postgresql.cnpg.io/v1   true         Backup
clusterimagecatalogs                postgresql.cnpg.io/v1   false        ClusterImageCatalog
clusters                            postgresql.cnpg.io/v1   true         Cluster
databaseroles                       postgresql.cnpg.io/v1   true         DatabaseRole
databases                           postgresql.cnpg.io/v1   true         Database
failoverquorums                     postgresql.cnpg.io/v1   true         FailoverQuorum
imagecatalogs                       postgresql.cnpg.io/v1   true         ImageCatalog
poolers                             postgresql.cnpg.io/v1   true         Pooler
publications                        postgresql.cnpg.io/v1   true         Publication
scheduledbackups                    postgresql.cnpg.io/v1   true         ScheduledBackup
subscriptions                       postgresql.cnpg.io/v1   true         Subscription
의미
NAMEREST 경로에 쓰이는 복수형입니다. /apis/postgresql.cnpg.io/v1/namespaces/<ns>/clusters
SHORTNAMES축약 별칭. 11개 전부 비어 있습니다
APIVERSION전부 /v1 입니다. alpha나 beta가 하나도 없다는 건 API가 안정 단계라는 뜻입니다
NAMESPACED네임스페이스 범위 여부. clusterimagecatalogsfalse 입니다
KINDYAML의 kind: 에 적는 이름

실무에서 쓰이는 지점은 두 개입니다.

첫째, 오퍼레이터 업그레이드 후 가장 싼 헬스체크 입니다. get crd | grep cnpg.io 의 개수와 api-resources 의 개수를 대조해서 어긋나면, CRD는 들어왔는데 서빙이 안 되는 상태입니다. 위 출력은 11과 11로 일치하니 정상입니다.

둘째, SHORTNAMES 부재가 이름 충돌을 부릅니다. kubectl get cluster 가 동작하는 것은 복수형이 clusters 이기 때문인데, Cluster API나 Rancher 같은 다른 오퍼레이터도 clusters 를 씁니다. 한 클러스터에 공존하면 명령이 모호해지거나 엉뚱한 API 그룹을 잡습니다. 외부에 넘기는 문서나 스크립트에는 clusters.postgresql.cnpg.io 처럼 풀네임을 쓰는 것이 안전합니다.

목록에서 읽어야 할 것

CRD 방식의 이점은 일반적으로 알려진 대로입니다. 쿠버네티스 표준 도구를 그대로 쓸 수 있고, 인프라 구성을 Git에 올려 GitOps 파이프라인을 태울 수 있으며, 오퍼레이터가 선언과 실제의 격차를 계속 추적하므로 자가 치유가 됩니다.

다만 이 목록에서 실무적으로 더 중요한 것은 두 가지입니다.

첫째, ObjectStore CRD가 없습니다. 백업 저장소 정의가 오퍼레이터 본체에서 빠져 있다는 뜻이고, plugin-barman-cloud 를 별도로 설치해야 생깁니다. 백업 구성이 오퍼레이터 안에서 플러그인으로 이관되는 중이라는 사실이 이 목록에 그대로 드러나 있습니다.

둘째, 11개 CRD가 곧 그 버전 CNPG의 기능 지도 입니다. 버전이 올라가면 여기가 늘어납니다. 1.30에서 새로 들어온 DatabaseRole 이 그 사례이고, 문서 목차보다 이 목록이 정확합니다.

오퍼레이터 설치 직후에 get crdapi-resources 를 떠 두면, 그 버전이 무엇을 할 수 있고 무엇을 못 하는지가 목록 하나로 드러납니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.