티스토리 뷰

안녕하세요. 이번 포스트는 쿠버네티스 오퍼레이터를 직접 구현해 보고, 개발한 오퍼레이터를 로컬 쿠버네티스 클러스터에 적용하는 과정까지 설명드리려고 합니다.


예전에 지형님이 소개했었던 쿠버네티스 오퍼레이터 적용하기를 본 독자들이 있을 겁니다. 오퍼레이터는 커스텀 리소스(Custom Resource, CR)를 사용하여 미리 구성된 리소스들(Deployment, Service 등)을 관리하는 쿠버네티스 익스텐션 리소스입니다.
커스텀 리소스 정의(Custom Resource Definition, CRD)를 통해 내가 만들고 싶은 커스텀 리소스의 스펙을 정의하고 오퍼레이터를 통해 커스텀 리소스의 세부 로직들을 수행합니다.



나만의 오퍼레이터를 직접 구현하기 위해 프로그래밍 언어별 SDK들이 몇 개가 있습니다.

 

  • Operator SDK (Go, Java)
  • kubebuilder (Go)
  • Kopf (Python)
  • KubeOps (.NET)
  • shell-operator (bash)
  • kube-rs (Rust)

이들 중에서 Go Operator SDK를 이용하여 쿠버네티스 오퍼레이터를 구현해 보겠습니다.


Operator SDK (Operator Framework)





Operator SDK는 오퍼레이터의 로직들을 High Level API로 추상화하여 쉽게 작성할 수 있는 프레임워크입니다.

Operator SDK의 주요 특징은 다음과 같습니다:

  • 프로젝트를 빠르게 생성하기 위한 코드들을 생성합니다.
  • Kubebuilder를 CLI 및 플러그인 기능을 위한 라이브러리로 활용합니다.
  • 모든 언어 기반 오퍼레이터 프로젝트는 Kubebuilder 표준을 따릅니다.
  • 클러스터의 리소스를 구성하는 데 사용되는 kustomize를 사용합니다.
  • 일반적인 오퍼레이터의 사용 사례를 다루기 위한 확장들을 제공합니다.



Operator SDK Workflow


Go 언어 기준으로 Operator SDK를 개발하는 워크 플로우는 다음과 같습니다.

  1. SDK CLI 명령어들을 이용해 오퍼레이터 프로젝트 생성
  2. CRD를 추가하여 새로운 리소스(CR) 정의
  3. 새로운 리소스들을 감시(watch)하고 조정(reconcile) 하기 위한 Controller 정의
  4. SDK와 컨트롤러 런타임 API를 사용하여 컨트롤러의 조정 로직을 작성
  5. SDK CLI를 사용하여 오퍼레이터 Deployment 매니페스트를 구축(build)하고 생성(generate)

위의 워크플로우를 토대로 오퍼레이터를 하나하나 원스텝으로 직접 구현해 보겠습니다.





로컬 개발 환경 구성

 

Golang 설치

 

Operator SDK에 필요한 Go를 설치합니다.

https://golang.org/doc/install 에 들어가서 운영체제에 맞는 패키지를 다운로드하여 설치를 진행합니다.



Operator SDK 설치

 

먼저 Operator SDK가 의존되어 있는 패키지 설치가 필요합니다.

아래는 제가 설치해 놓은 패키지의 버전들을 명시해 놓았습니다.

# git
$ git --version
git version 2.37.1

# go: 1.21+
$ go version
go version go1.21.3 darwin/arm64

# docker version 17.03+
$ docker version
# Rancher Desktop 1.9.1로 설치
Client:
 Version:           24.0.2-rd
 API version:       1.42 (downgraded from 1.43)
 Go version:        go1.20.4
 Git commit:        e63f5fa
 Built:             Fri May 26 16:40:56 2023
 OS/Arch:           darwin/arm64
 Context:           default

Server:
 Engine:
  Version:          23.0.6
  API version:      1.42 (minimum version 1.12)
  Go version:       go1.20.4
  Git commit:       9dbdbd4b6d7681bd18c897a6ba0376073c2a72ff
  Built:            Fri May 12 13:54:36 2023
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v1.7.0
  GitCommit:        1fbd70374134b891f97ce19c70b6e50c7b9f4e0d
 runc:
  Version:          1.1.7
  GitCommit:        860f061b76bb4fc671f0f9e900f7d80ff93d4eb7
 docker-init:
  Version:          0.19.0
  GitCommit:

# kubectl: 쿠버네티스 클러스터
$ kubectl version --short
Client Version: v1.27.4
Kustomize Version: v5.0.1
Server Version: v1.27.1

다음과 같이 operator-sdk CLI를 설치합니다.

# Operator SDK CLI 설치
$ brew install operator-sdk



로컬 쿠버네티스 클러스터 설치

 

로컬에 간단한 쿠버네티스 클러스터를 빠르게 생성하기 위해 kind를 이용하여 docker 컨테이너 기반으로 쿠버네티스 클러스터를 구성합니다.

docker는 Rancher Desktop 1.9.1 버전을 설치하여 구성하였습니다.

# kind CLI (requires go 1.61+)
$ go install sigs.k8s.io/kind@v0.19.0

# create kind cluster
$ kind create cluster --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: my-cluster
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
- role: worker
- role: worker
networking:
  apiServerAddress: "127.0.0.1"
  apiServerPort: 6443
  disableDefaultCNI: true
  podSubnet: 192.168.0.0/16
EOF

# install calico
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/tigera-operator.yaml
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/custom-resources.yaml

# install ingress nginx controller
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

# 로컬 쿠버네티스 클러스터 노드 확인
$ kubectl get node
NAME                       STATUS   ROLES           AGE    VERSION
NAME                       STATUS     ROLES           AGE   VERSION
my-cluster-control-plane   NotReady   control-plane   84s   v1.27.1
my-cluster-worker          NotReady   <none>          60s   v1.27.1
my-cluster-worker2         NotReady   <none>          61s   v1.27.1




SDK CLI 명령어들을 이용해 오퍼레이터 프로젝트 생성

 

Spring-Framework-Petclinic이라는 Spring Boot 기반의 샘플 데모 웹 애플리케이션을 로컬 쿠버네티스 클러스터에 Petclinic이라는 리소스에 Deployment, Service, Ingress로 구성하여 배포하는 오퍼레이터를 만들어 보겠습니다.

 

 

사전 작업으로 터미널을 켜서 다음과 같이 진행합니다.

# 먼저 go 프로젝트의 워킹 디렉토리로 이동합니다.
$ echo $GOPATH

# operator들마다 subdirectory로 나눌 수 있도록 디렉토리를 생성합니다.
$ mkdir -p $GOPATH/src/petclinic
$ cd $GOPATH/src/petclinic

# petclinic-operator 라는 이름으로 프로젝트를 생성합니다.
$ operator-sdk init --domain my.domain --project-name petclinic-operator --owner "gyumipark"

CLI에서 오퍼레이터 프로젝트를 생성해 주는 operator-sdk init 명령어에는 다양한 옵션들이 있습니다.

 

  • --domain {string}: 그룹 도메인 지정 (default: "my.domain")
  • --license {string}: 보일러플레이트에 사용될 라이선스. "apache" 또는 "none"이 들어갈 수 있습니다. (default: "apache2")
  • --owner {string}: copyright에 추가할 소유자
  • --project-name {string}: 프로젝트 명
  • --project-version {string}: 프로젝트 버전 (default: "3")
  • --repo {string}: go 모듈에 사용할 이름 (default: 현재 워킹 디렉터리의 go 패키지명)
  • --plugins {stings}: operator-sdk CLI의 서브커맨드에 사용될 플러그인 키를 지정합니다. (default: "go.kubebuilder.io/v4")



CRD를 추가하여 새로운 리소스 정의

 

Petclinic이라는 리소스를 v1beta1 버전으로 새로 정의하고 컨트롤러에 대한 스켈레톤 코드를 생성합니다.

$ operator-sdk create api --group spring --version v1beta1 --kind Petclinic
Create Resource [y/n]
y
Create Controller [y/n]
y
...

이제부터는 본격적으로 코드를 수정해야 하는데, 다음과 같은 yaml 형식으로 하나의 리소스로만 쿠버네티스에서 petclinic 애플리케이션을 실행할 수 있도록 구현할 것입니다.

apiVersion: spring.my.domain/v1beta1
kind: Petclinic
metadata:
  name: petclinic
  namespace: petclinic
spec:
  image: springcommunity/spring-framework-petclinic:latest
  size: 1
  port: 80

먼저 config/samples/spring_v1beta1_petclinic.yaml에서 컨트롤러 함수에 필요한 사용자 지정값들을 추가합니다.

apiVersion: spring.my.domain/v1beta1
kind: Petclinic
metadata:
  labels:
    app.kubernetes.io/name: petclinic
    app.kubernetes.io/instance: petclinic-sample
    app.kubernetes.io/part-of: petclinic-operator
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: petclinic-operator
  name: petclinic-sample
spec:
  image: springcommunity/spring-framework-petclinic:latest
  size: 1
  port: 80

이제 컨트롤러에 사용자 지정 값들을 가져오기 위해 api/v1beta1/petclinic_types.go에 PetclinicSpec에 아까 추가한 사용자 지정 값들에 대한 변수를 정의합니다.

type PetclinicSpec struct {
   // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
   // Important: Run "make" to regenerate code after modifying this file

   // Foo is an example field of Petclinic. Edit petclinic_types.go to remove/update
   Size  int32  `json:"size"`
   Image string `json:"image"`
   Port  int32  `json:"port"`
}




Controller 로직 작성

 

다음은 컨트롤러의 로직을 구현해야 합니다.

이미 Operator SDK에서 컨트롤러 파일과 내부 Reconcile 함수가 만들어져 있는데요. 이 함수는 쿠버네티스가 오브젝트의 현재 상태와 원하는 상태 차이를 감지할 때마다 control loop에 의해 호출됩니다.

 

 

SDK가 생성한 main.go 파일에 Petclinic 리소스와 컨트롤러 사이의 연결해 주는 보일러플레이트가 있어 개발자 입장에서 건들 필요가 없고, 컨트롤러 로직에 집중하고 작동하는 것에 필요한 모든 방대한 세부 사항을 추상화가 가능합니다.

그러면 Petclinic이라는 커스텀 리소스의 Deployment, Service, Ingress 생성 로직을 구현하고, Reconcile 함수에 포함하는 것까지 차례대로 설명드리겠습니다.



Deployment

func (r *PetclinicReconciler) createOrUpdateDeployment(ctx context.Context, app *springv1beta1.Petclinic) error {
   deployment := appsv1.Deployment{
      ObjectMeta: metav1.ObjectMeta{
         Name:      app.ObjectMeta.Name,
         Namespace: app.ObjectMeta.Namespace,
         Labels:    map[string]string{"label": app.ObjectMeta.Name, "app": app.ObjectMeta.Name},
      },
      Spec: appsv1.DeploymentSpec{
         Replicas: &app.Spec.Size,
         Selector: &metav1.LabelSelector{
            MatchLabels: map[string]string{"label": app.ObjectMeta.Name},
         },
         Template: v1.PodTemplateSpec{
            ObjectMeta: metav1.ObjectMeta{
               Labels: map[string]string{"label": app.ObjectMeta.Name, "app": app.ObjectMeta.Name},
            },
            Spec: v1.PodSpec{
               Containers: []v1.Container{
                  {
                     Name:  app.ObjectMeta.Name + "-container",
                     Image: app.Spec.Image,
                     Ports: []v1.ContainerPort{
                        {
                           ContainerPort: app.Spec.Port,
                        },
                     },
                  },
               },
            },
         },
      },
   }

   _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &deployment, func() error {
      return nil
   })
   if err != nil {
      return fmt.Errorf("Deployment를 가져올 수 없습니다: %v", err)
   }
   return nil
}



Service

func (r *PetclinicReconciler) createOrUpdateService(ctx context.Context, app *springv1beta1.Petclinic) error {
   service := v1.Service{
      ObjectMeta: metav1.ObjectMeta{
         Name:      app.ObjectMeta.Name,
         Namespace: app.ObjectMeta.Namespace,
         Labels:    map[string]string{"app": app.ObjectMeta.Name},
      },
      Spec: v1.ServiceSpec{
         Type:                  v1.ServiceTypeNodePort,
         ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
         Selector:              map[string]string{"app": app.ObjectMeta.Name},
         Ports: []v1.ServicePort{
            {
               Name:       "http",
               Port:       app.Spec.Port,
               Protocol:   v1.ProtocolTCP,
               TargetPort: intstr.FromInt(int(app.Spec.Port)),
            },
         },
      },
      Status: v1.ServiceStatus{},
   }

   _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &service, func() error {
      return nil
   })

   if err != nil {
      return fmt.Errorf("Service를 생성할 수 없습니다!: %v", err)
   }
   return nil
}



Ingress

func (r *PetclinicReconciler) createOrUpdateIngress(ctx context.Context, app *springv1beta1.Petclinic) error {
   var ingressClassName = "nginx"
   var pathType = networkingv1.PathTypePrefix

   ingress := networkingv1.Ingress{
      ObjectMeta: metav1.ObjectMeta{
         Name:      app.ObjectMeta.Name,
         Namespace: app.ObjectMeta.Namespace,
         Labels:    map[string]string{"app": app.ObjectMeta.Name},
      },
      Spec: networkingv1.IngressSpec{
         IngressClassName: &ingressClassName,
         Rules: []networkingv1.IngressRule{
            {
               IngressRuleValue: networkingv1.IngressRuleValue{
                  HTTP: &networkingv1.HTTPIngressRuleValue{
                     Paths: []networkingv1.HTTPIngressPath{
                        {
                           Path:     "/",
                           PathType: &pathType,
                           Backend: networkingv1.IngressBackend{
                              Service: &networkingv1.IngressServiceBackend{
                                 Name: app.ObjectMeta.Name,
                                 Port: networkingv1.ServiceBackendPort{
                                    Number: app.Spec.Port,
                                 },
                              },
                           },
                        },
                     },
                  },
               },
            },
         },
      },
   }

   _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &ingress, func() error {
      return nil
   })

   if err != nil {
      return fmt.Errorf("Ingress를 생성할 수 없습니다!: %v", err)
   }
   return nil
}



Petclinic 리소스 삭제

 

finalizer를 구성하기 위해 Petclinic 리소스에 대한 삭제 함수를 추가합니다.

finalizer는 쿠버네티스가 오브젝트를 완전히 삭제하기 이전에 삭제했다는 표시를 위해 특정 조건이 충족될 때까지 대기하도록 알려주기 위한 네임스페이스에 속한 키입니다.
finalizer는 삭제 완료된 오브젝트가 소유한 리소스를 정리하기 위해 컨트롤러에 알립니다.

func (r *PetclinicReconciler) reconcileDelete(ctx context.Context, app *springv1beta1.Petclinic) (ctrl.Result, error) {
   l := log.FromContext(ctx)

   l.Info("removing application")

   controllerutil.RemoveFinalizer(app, finalizer)
   err := r.Update(ctx, app)
   if err != nil {
      return ctrl.Result{}, fmt.Errorf("Error removing finalizer %v", err)
   }
   return ctrl.Result{}, nil
}



Reconcile 함수에 연결

마지막으로 여태까지 만들어 놓은 함수들을 Reconcile 함수에 추가합니다.

Reconcile 함수에 대한 최종적인 코드는 다음과 같습니다.


const finalizer = "spring.my.domain/application_controller_finalizer"

...

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

   var app springv1beta1.Petclinic

   if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
      if errors.IsNotFound(err) {
         return ctrl.Result{}, nil
      }
      l.Error(err, "Petclinic을 가져올 수 없습니다!")
      return ctrl.Result{}, err
   }

   /*
        finalizer는 쿠버네티스에게 오브젝트 삭제에 대한 통제가 필요하다는 것을 알려주기 때문에 필수!
        finalizer가 없으면, 쿠버네티스 가비지 컬렉터를 삭제할 수 없고 클러스터에 쓸모없는 리소스를 가질 위험이 있다.
   */
   if !controllerutil.ContainsFinalizer(&app, finalizer) {
      l.Info("Finalizer 추가")
      controllerutil.AddFinalizer(&app, finalizer)
      return ctrl.Result{}, r.Update(ctx, &app)
   }

   if !app.DeletionTimestamp.IsZero() {
      l.Info("Petclinic 삭제")
      return r.reconcileDelete(ctx, &app)
   }

   l.Info("Petclinic 생성")

   l.Info("Deployment 생성")
   err := r.createOrUpdateDeployment(ctx, &app)
   if err != nil {
      return ctrl.Result{}, err
   }
   l.Info("Service 생성")
   err = r.createOrUpdateService(ctx, &app)
   if err != nil {
      return ctrl.Result{}, err
   }
   l.Info("Ingress 생성")
   err = r.createOrUpdateIngress(ctx, &app)
   if err != nil {
      return ctrl.Result{}, err
   }

   return ctrl.Result{}, nil
}



Deploy petclinic operator

 

로컬 쿠버네티스 클러스터에 배포해 보기 전에 몇 가지 작업을 진행합니다.

config/manager/manager.yaml에 controller 이미지 명을 다음과 같이 수정합니다.

저는 docker hub에 있는 제 계정 repo에 push할 예정이라서 이미지 명을 ycatt/petclinic-operator:latest로 수정했습니다.

spec:
  selector:
    matchLabels:
      control-plane: controller-manager
  replicas: 1
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        control-plane: controller-manager
    spec:
      ...
      containers:
      - command:
        - /manager
        args:
        - --leader-elect
        image: ycatt/petclinic-operator:latest
        name: manager
        ...

그리고 터미널에서 몇 개의 명령어들을 입력합니다.

# *_types.go 파일을 수정한 후, 해당 리소스 타입에 대해 생성된 코드를 업데이트합니다.
$ make generate
# CRD 매니페스트를 업데이트합니다.
$ make manifests

# petclinic operator를 로컬 도커 레지스트리에 이미지 빌드합니다.
$ docker build -t petclinic-operator .

# public docker hub에 올리기 위해 태그를 생성합니다.
$ docker tag petclinic-operator:latest ycatt/petclinic-operator:latest

# public docker hub에 login 한 후에 petclinic operator 이미지를 push 합니다.
$ docker login
$ export IMG=ycatt/petclinic-operator:latest
$ docker push ycatt/petclinic-operator:latest

make deploy 명령어를 이용하면 로컬 쿠버네티스 클러스터에 Petclinic Operator Controller가 배포되는 것을 확인할 수 있습니다.

$ make deploy

$ kubectl config set-context --current --namespace=petclinic-operator-system

$ kubectl get pod
NAME                                                     READY   STATUS    RESTARTS   AGE
petclinic-operator-controller-manager-549b647fbc-9lkx4   2/2     Running   0          2m36s

그다음에는 config/samples에 있는 Petclinic CR을 직접 올려봅니다.

# 먼저 kind 클러스터에 system:serviceaccounts group에 대한 cluster-admin RBAC을 생성합니다.
$ kubectl create clusterrolebinding serviceaccounts-cluster-admin \
  --clusterrole=cluster-admin \
  --group=system:serviceaccounts

$ kubectl apply -n default -f - <<EOF
apiVersion: spring.my.domain/v1beta1
kind: Petclinic
metadata:
  labels:
    app.kubernetes.io/name: petclinic
    app.kubernetes.io/instance: petclinic-sample
    app.kubernetes.io/part-of: petclinic-operator
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: petclinic-operator
  name: petclinic-sample
  namespace: default
spec:
  image: springcommunity/spring-framework-petclinic:latest
  size: 1
  port: 80
EOF

# CR로 생성된 워크로드들을 확인합니다.
$ kubectl get all -n default
NAME                                    READY   STATUS    RESTARTS   AGE
pod/petclinic-sample-868d5c45d6-88wg2   1/1     Running   0          32s

NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/kubernetes         ClusterIP   10.96.0.1      <none>        443/TCP          8m55s
service/petclinic-sample   NodePort    10.96.228.29   <none>        8080:32672/TCP   32s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/petclinic-sample   1/1     1            1           32s

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/petclinic-sample-868d5c45d6   1         1         1       32s

# CR로 생성된 ingress를 확인합니다.
$ kubectl get ing -n default
NAME               CLASS   HOSTS   ADDRESS     PORTS   AGE
petclinic-sample   nginx   *       localhost   80      37s

# petclinic operator에서의 controller manager pod 확인
$ kubectl get pod -n petclinic-operator-system

# petclinic operator 로그 확인
$ kubectl logs petclinic-operator-controller-manager-549b647fbc-8drtq -n petclinic-operator-system 
...
2024-02-07T04:28:42Z    INFO    Finalizer 추가  {"controller": "petclinic", "controllerGroup": "spring.my.domain", "controllerKind": "Petclinic", "Petclinic": {"name":"petclinic-sample","namespace":"default"}, "namespace": "default", "name": "petclinic-sample", "reconcileID": "fc5991fe-a86d-4eae-b490-f55bce404ac2"}
2024-02-07T04:28:42Z    INFO    Petclinic 생성  {"controller": "petclinic", "controllerGroup": "spring.my.domain", "controllerKind": "Petclinic", "Petclinic": {"name":"petclinic-sample","namespace":"default"}, "namespace": "default", "name": "petclinic-sample", "reconcileID": "a5206c8a-fe44-4ebe-9af3-2dc0f95e211c"}
2024-02-07T04:28:42Z    INFO    Deployment 생성 {"controller": "petclinic", "controllerGroup": "spring.my.domain", "controllerKind": "Petclinic", "Petclinic": {"name":"petclinic-sample","namespace":"default"}, "namespace": "default", "name": "petclinic-sample", "reconcileID": "a5206c8a-fe44-4ebe-9af3-2dc0f95e211c"}
2024-02-07T04:28:42Z    INFO    Service 생성    {"controller": "petclinic", "controllerGroup": "spring.my.domain", "controllerKind": "Petclinic", "Petclinic": {"name":"petclinic-sample","namespace":"default"}, "namespace": "default", "name": "petclinic-sample", "reconcileID": "a5206c8a-fe44-4ebe-9af3-2dc0f95e211c"}
2024-02-07T04:28:42Z    INFO    Ingress 생성    {"controller": "petclinic", "controllerGroup": "spring.my.domain", "controllerKind": "Petclinic", "Petclinic": {"name":"petclinic-sample","namespace":"default"}, "namespace": "default", "name": "petclinic-sample", "reconcileID": "a5206c8a-fe44-4ebe-9af3-2dc0f95e211c"}

브라우저에 http://localhost 로 접속하여 petclinic 메인 화면이 나오는지 확인합니다.

 



Project Layout

 

최종적으로 오퍼레이터 프로젝트의 레이아웃 트리는 다음과 같습니다.

.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── api
│   └── v1beta1
│       ├── groupversion_info.go
│       ├── petclinic_types.go
│       └── zz_generated.deepcopy.go
├── bin
│   └── controller-gen
├── cmd
│   └── main.go
├── config
│   ├── crd
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches
│   │       ├── cainjection_in_petclinics.yaml
│   │       └── webhook_in_petclinics.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── manifests
│   │   └── kustomization.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── petclinic_editor_role.yaml
│   │   ├── petclinic_viewer_role.yaml
│   │   ├── role_binding.yaml
│   │   └── service_account.yaml
│   ├── samples
│   │   ├── kustomization.yaml
│   │   └── spring_v1beta1_petclinic.yaml
│   └── scorecard
│       ├── bases
│       │   └── config.yaml
│       ├── kustomization.yaml
│       └── patches
│           ├── basic.config.yaml
│           └── olm.config.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── internal
    └── controller
        ├── petclinic_controller.go
        └── suite_test.go

프로젝트 트리 내에서 구성된 여러 가지의 디렉토리에 대한 설명은 다음과 같습니다:

 

  • Dockerfile: 직접 구현한 Operator를 docker build 하기 위한 도커파일
  • Makefile: 오퍼레이터 프로젝트 배포를 돕기 위한 도우미 파일
  • PROJECT: 프로젝트의 구성 파일
  • bin/: 프로젝트를 로컬 실행하는 데 사용되는 Manager와 프로젝트 구성에 사용되는 kustomize 유틸리티 파일들
  • config/: 클러스터에서 프로젝트를 시작하기 위한 구성 파일과 Kustomize YAML 파일들
    • config/crd/: Custom Resource Definition 파일
    • config/manager/: 클러스터에서 오퍼레이터 프로젝트를 워크로드로 시작하기 위한 매니페스트 파일
    • config/rbac/: 프로젝트를 돌리는 데 필요한 RBAC 권한 매니페스트 파일들
    • config/samples/: Custerom Resource 파일
    • config/manifests/: bundle 디렉토리에 OLM 매니페스트를 생성하기 위한 기반 파일들
    • config/prometheus/: 프로젝트가 ServiceMonitor 리소스와 같은 Prometheus에 메트릭을 제공할 수 있도록 하는 데 필요한 매니페스트 파일
    • config/scorecard/: 프로젝트를 Scorecard로 테스트하는 데 필요한 매니페스트 파일들
  • api/: api 정의
  • hack/: 프로젝트 파일의 라이센스 헤더를 스캐폴딩(자동 생성)에 사용되는 파일과 같은 유틸리티 파일들
  • cmd/main.go: 프로젝트 초기화 구현 코드
  • internal/controller: 컨트롤러 구현 코드

상당히 복잡해 보이지만, 이번 포스트에서 실질적으로 건드렸던 파일들은 몇 개 없었습니다. 이런 구조로 되어 있다고 참고하시면 되겠습니다.



마치면서...

 

Golang기반 Operator SDK를 사용하여 오퍼레이터를 직접 개발하는 여정을 마쳤습니다.
쿠버네티스 클러스터에서 나만의 사용자 정의 리소스와 컨트롤러를 구축하여 애플리케이션을 보다 효율적으로 관리하고 운영할 수 있다는 사실만 잊지 않으셨으면 합니다.
다음 편에서는 다른 프로그래밍 언어인 Java 기반으로 Operator를 구현해 보는 포스트로 뵙겠습니다.



참고




댓글