k8s

k8s의 CRD 만들기

Custom Resource Definition

by HOON


1

Last updated on Feb. 6, 2025, 9:16 p.m.


random_image

안녕하세요. 저번 포스트에서 k8s의 Deployment, ReplicaSet을 직접 생성하고 테스트하는 내용을 작성했었습니다. 👉🏻 Deployment,RecpliaSet 테스트

오늘은 이어서, 테스트를 위해 kubebuilder, go언어를 설치한김에 Custom Resource Definition을 작성 후 테스트까지 해볼까 합니다.
간단한 ConfigMap으로 동작하는 리소스를 예시로 만들어볼게요.



1. 설치 환경

우선 테스트를 위해 kubernetes cluster와 Go언어 개발 환경, kubebuilder 설치가 필요합니다.
제 환경은 VirtualBox의 ubuntu22 릴리즈 버전을 사용하고있습니다. master,node1,node2로 이루어져있지만 master에서만 실행해보겠습니다.
추가로 해당 vm의 용량은 50GB로 잡았습니다.

글은 명령어 이미지-> 복사 가능한 코드 순서로 작성해두겠습니다


2. 프로젝트 디렉토리 및 초기화

테스트를 위한 폴더 생성 및 초기화를 진행해볼게요.

mkdir simple-operator
cd simple-operator

# 저는 go버전이 달라서 버전 스킵 옵션을 사용했습니다. 현재 버전:go1.21.5, 필요 버전: go1.23
k8s-master@k8s-master:~/simple-operator$ kubebuilder init --skip-go-version-check --domain=mydomain.io --repo=simple-operator 
#--domain 옵션은 Custom Resource의 그룹 도메인을 결정합니다. 예시로 mydomain.io라고 설정했습니다.

이 작업을 진행하면 go.mod , 주요 폴더 구조(config, contollers , api 등) 이 생성됩니다.



3. API(= CRD) 및 Controller 코드 생성

k8s-master@k8s-master:~/simple-operator$ kubebuilder create api --group=sample --version=v1alpha1 --kind=Hello
#--group=sample: 그룹은 보통 도메인 하위로 sample.mydomain.io 이런 식으로 생성될 것입니다.
#--version=v1alpha1: CRD 버전을 v1alpha1로 지정합니다.
#--kind=Hello: Custom Resource의 Kind를 Hello라고 지었고, 이를 통해 CR 이름은 hellos.sample.mydomain.io 형태가 됩니다.



4. API(= CRD) 구조 정의

이제, API 구조 정의를 위해서 경로를 이동해보겠습니다.
api/v1alpha1/hello_types.go 파일을 열어보면, HelloSpec과, HelloStatus가 존재할텐데 이 내용을 수정 해 볼게요.

// HelloSpec defines the desired state of Hello
type HelloSpec struct {
    // 원하는 메시지
    Message string `json:"message,omitempty"`
}

// HelloStatus defines the observed state of Hello
type HelloStatus struct {
    // 현재 생성된 ConfigMap 이름(예시)
    ConfigMapName string `json:"configMapName,omitempty"`
}

이 구조를 정의하면 Spec 과 Status를 커스텀 정의 할 수 있습니다. 예를들어, spec에 Message를 정의 해 뒀다면,
아래와 같이 실제 메니페스트 파일을 작성할때 사용 가능합니다.

apiVersion: mygroup.example.com/v1alpha1
kind: Hello
metadata:
  name: my-hello
spec:
  message: "Hello, Kubernetes!"

다음은, Controller 로직을 작성 해보겠습니다.
우선 다음 경로로 이동하겠습니다. internal/controller/hello_controller.go (kubebuilder 버전에 따라 controllers/hello_controller.go 경로가 될 수도 있습니다.)

func (r *HelloReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := ctrllog.FromContext(ctx)

    // 1) CR(Hello) 가져오기
    var hello samplev1alpha1.Hello
    if err := r.Get(ctx, req.NamespacedName, &hello); err != nil {
        // CR이 삭제된 경우  NotFound 에러면 무시
        if errors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // 2) CR.spec.message를 참조하여 ConfigMap 생성/업데이트
    configMapName := hello.Name + "-configmap"

    // ConfigMap 원하는 spec
    desiredConfigMap := &corev1.ConfigMap{
        ObjectMeta: metav1.ObjectMeta{
            Name:      configMapName,
            Namespace: hello.Namespace,
        },
        Data: map[string]string{
            "message": hello.Spec.Message,
        },
    }

    // ConfigMap 실제 상태 가져오기
    var existingConfigMap corev1.ConfigMap
    err := r.Get(ctx, types.NamespacedName{
        Name:      configMapName,
        Namespace: hello.Namespace,
    }, &existingConfigMap)

    if err != nil && errors.IsNotFound(err) {
        // 없으면 만들기
        if createErr := r.Create(ctx, desiredConfigMap); createErr != nil {
            return ctrl.Result{}, createErr
        }
        log.Info("Created ConfigMap", "ConfigMapName", configMapName)
    } else if err == nil {
        // 이미 있으면, Data만 업데이트
        existingConfigMap.Data = desiredConfigMap.Data
        if updateErr := r.Update(ctx, &existingConfigMap); updateErr != nil {
            return ctrl.Result{}, updateErr
        }
        log.Info("Updated ConfigMap", "ConfigMapName", configMapName)
    } else {
        return ctrl.Result{}, err
    }

    // 3) CR Status 업데이트(옵션)
    if hello.Status.ConfigMapName != configMapName {
        hello.Status.ConfigMapName = configMapName
        if statusErr := r.Status().Update(ctx, &hello); statusErr != nil {
            return ctrl.Result{}, statusErr
        }
    }

    return ctrl.Result{}, nil
}
  1. CR 가져오기 -> 2. ConfigMap 생성, 실제 상태 가져오기 -> 3. CR Status 업데이트의 순으로 이루어져있습니다.



5. Makefile 이용해서 CRD 생성 및 Controller 배포

처음 kubebuilder create api 명령 이후, config/ 디렉터리 안에 CRD, RBAC, Kustomize 관련 매니페스트들이 생성되어 있습니다.
따라서 make install 명령어를 실행하게되면 config/crd/bases/ 아래에 정의된 CRD yaml(hellos.sample.mydomain.io.yaml)을 클러스터에 적용 할 수 있습니다.
먼저 make install 명령어로 CRD를 생성합니다.

다음, Controller 배포를 위해 make deploy 명령을 실행하여, config/default/ 아래 있는 내용들을 kustomize로 빌드 후 클러스터에 배포합니다.



6. CR(Hello) 생성하여 동작 확인

자 이제, 코드는 완성됐고 배포까지 됐으니 정상작동이 되는지 확인 한번 해볼까요??
manifest 파일을 한번 작성 해 볼게요.

apiVersion: sample.mydomain.io/v1alpha1 #위에서 정의한 domain이름 내용을 토대로 apiVersion을 작성 할 수 있습니다.
kind: Hello #저희가 작성한 ConfigMap의 기능을 하는 새로운 리소스 Hello를 정의합니다.
metadata:
  name: hello-sample
  namespace: default
spec:
  message: "Hello from CR!"  #작성된 API 구조 정의된 내용을 토대로 작성 할 수 있습니다.

이제 명령어를 통해 리소스를 생성해볼게요.

kubectl apply -f hello-sample.yaml
kubectl get hello hello-sample -o yaml

적용이 됐으니 한번 실행중인지 살펴볼게요!!!



7. 에러 해결

Pending?? 이상하네요.. 한번 describe 해볼까요?

k8s-master@k8s-master:~/simple-operator$ kubectl get pods -n simple-operator-system
NAME                                                  READY   STATUS    RESTARTS   AGE
simple-operator-controller-manager-6f9c5ff4d5-dv2rl   0/1     Pending   0          12m
k8s-master@k8s-master:~/simple-operator$ kubectl logs simple-operator-controller-manager-6f9c5ff4d5-dv2rl -n simple-operator-system
k8s-master@k8s-master:~/simple-operator$ kubectl describe pod simple-operator-controller-manager-6f9c5ff4d5-dv2rl -n simple-operator-system
Name:             simple-operator-controller-manager-6f9c5ff4d5-dv2rl
Namespace:        simple-operator-system
Priority:         0
Service Account:  simple-operator-controller-manager
Node:             <none>
Labels:           app.kubernetes.io/name=simple-operator
                  control-plane=controller-manager
                  pod-template-hash=6f9c5ff4d5
Annotations:      kubectl.kubernetes.io/default-container: manager
Status:           Pending

. . .

QoS Class:                   Burstable
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason            Age                    From               Message
  ----     ------            ----                   ----               -------
  Warning  FailedScheduling  13m                    default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) had untolerated taint {node.kubernetes.io/unreachable: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
  Warning  FailedScheduling  3m34s (x2 over 8m34s)  default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) had untolerated taint {node.kubernetes.io/unreachable: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.

아, Events를 보니 untolerated taint {node.kubernetes.io/unreachable: } taint가 적용되어있어서 에러가 발생한거네요. CKA 단골문제였습니다..
그럼 방법은 2가지가 있습니다.

1. toleration을 추가하기
2. node의 taint 삭제하기

저는 2번은 테스트용으로는 간편해서 좋지만, 보안상 위협이 생길 수 있다 판단하여 1번으로 진행하겠습니다.

control-plane taint에 대해 toleration을 추가하기위해, Operator의 매니페스트(일반적으로 config/manager/manager.yaml 또는 config/default/manager.yaml 파일)를 열어 아래와 같이 수정합니다.

spec:
  template:
    spec:
      tolerations:
        - key: "node-role.kubernetes.io/control-plane"
          operator: "Exists"
          effect: "NoSchedule"

이제 다시 한 번 점검 해볼게요.

kubectl get nodes -A

이상없이 Running중인걸 확인 할 수 있습니다!
테스트는 여기까지 하겠습니다.👏🏻


8. 마무리

이번 포스트는 기존에 존재하는 리소스처럼 작동하는 custom resource를 작성하고 배포하여 테스트까지 해봤습니다.
이제 어느정도 리소스들이 작동하는 원리 기본개념을 좀 익힌 것 같아요.
물론 ConfigMap이라는 아주 단순한 리소스를 대상으로 했지만,
추후 제가 진짜 필요한 리소스 (redis 혹은 postgresql 등) 이 생긴다면 조금 더 복잡하겠지만 도전 해 볼 수 있겠죠??

이번 포스트를 진행하면서 에러가 꽤 많았지만 (vm 디스크 용량부족, image build 에러 등) 여기선 다루지 않았습니다.
추후에 다뤄보도록 하겠습니다. 감사합니다.

×
k8s linux


Leave a Comment: