Kubernetes 이야기

Kubebuilder ( Kubernetes operator ) 본문

개발/go

Kubebuilder ( Kubernetes operator )

kmaster 2022. 8. 17. 15:05
반응형

 

오퍼레이터(Operator)는 사용자 정의 리소스 (CR)를 사용하여 애플리케이션 및 해당 컴포넌트를 관리하는 쿠버네티스의 소프트웨어 확장 기능이다. 오퍼레이터는 쿠버네티스 원칙, 특히 컨트롤 루프를 따른다.

 

오퍼레이터 패턴은 서비스 또는 서비스 셋을 관리하는 운영자의 주요 목표를 포착하는 것을 목표로 한다. 특정 애플리케이션 및 서비스를 돌보는 운영자는 시스템의 작동 방식, 배포 방법 및 문제가 있는 경우 대처 방법에 대해 깊이 알고 있다.

 

예를 들어, Kubernetes에서 MySQL , Elasticsearch 또는 Gitlab 실행기 와 같은 도구를 배포하고 유지 관리하는 운영자를 상상할 수 있다 . 운영자는 이러한 도구를 구성하고 이벤트에 따라 시스템 상태를 조정하고 장애에 대응할 수 있다.

 

출처 : https://cloud.redhat.com/blog/build-your-kubernetes-operator-with-the-right-tool

Operator는 사용자 지정 리소스 정의(CRD)와 컨트롤러의 두 가지 구성 요소로 구성된다. CRD는 사양 및 상태를 설명하는 데 사용되는 "Kubernetes 사용자 지정 유형" 또는 리소스의 청사진이다. 사용자 지정 리소스(또는 CR)라고 하는 CRD의 인스턴스를 정의할 수 있다. 컨트롤러(제어 루프라고도 함)는 클러스터의 상태를 지속적으로 모니터링하고 이벤트에 따라 변경한다. 목표는 리소스의 현재 상태를 사용자 지정 리소스 사양 (CR)에서 사용자가 정의한 원하는 상태로 가져오는 것이다.

 

출처 : https://www.oreilly.com/library/view/programming-kubernetes/9781492047094/ch01.html

 

일반적으로 컨트롤러는 리소스 유형에 따라 다르지만 다양한 리소스 집합에 대해 CRUD(Create, Read, Update 및 Delete) 작업을 수행할 수 있다.

 

개발자가 쉽게 오퍼레이터를 개발하기 위해서 Go 언어는 주로 Kubebuilder 나 OperatorSDK를 사용하여 개발한다.

참고로 Python에서는 KOPF (https://github.com/nolar/kopf) 를 사용하여 개발한다.

Kubebuilder 를 사용하여 Operator를 만들어 보자.

 

설치

Kubebuilder를 설치 전 사전 설치 조건을 살펴보자.

 

  • 버전 v1.17.9 이상 으로 이동
  • 도커 버전 17.03 이상.
  • kubectl 버전 v1.11.3 이상.
  • Kubernetes v1.11.3+ 클러스터에 대한 액세스.
# curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
# chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
# kubebuilder version
Version: main.version{KubeBuilderVersion:"3.6.0", KubernetesVendor:"1.24.1", GitCommit:"f20414648f1851ae97997f4a5f8eb4329f450f6d", BuildDate:"2022-08-03T11:47:17Z", GoOs:"linux", GoArch:"amd64"}

# kubebuilder
CLI tool for building Kubernetes extensions and tools.

Usage:
  kubebuilder [flags]
  kubebuilder [command]

Examples:
The first step is to initialize your project:
    kubebuilder init [--plugins=<PLUGIN KEYS> [--project-version=<PROJECT VERSION>]]

<PLUGIN KEYS> is a comma-separated list of plugin keys from the following table
and <PROJECT VERSION> a supported project version for these plugins.

                              Plugin keys | Supported project versions
------------------------------------------+----------------------------
                base.go.kubebuilder.io/v3 |                          3
         declarative.go.kubebuilder.io/v1 |                       2, 3
  deploy-image.go.kubebuilder.io/v1-alpha |                          3
                     go.kubebuilder.io/v2 |                       2, 3
                     go.kubebuilder.io/v3 |                          3
               go.kubebuilder.io/v4-alpha |                          3
          grafana.kubebuilder.io/v1-alpha |                          3
       kustomize.common.kubebuilder.io/v1 |                          3
 kustomize.common.kubebuilder.io/v2-alpha |                          3

For more specific help for the init command of a certain plugins and project version
configuration please run:
    kubebuilder init --help --plugins=<PLUGIN KEYS> [--project-version=<PROJECT VERSION>]

Default plugin keys: "go.kubebuilder.io/v3"
Default project version: "3"


Available Commands:
  alpha       Alpha-stage subcommands
  completion  Load completions for the specified shell
  create      Scaffold a Kubernetes API or webhook
  edit        Update the project configuration
  help        Help about any command
  init        Initialize a new project
  version     Print the kubebuilder version

Flags:
  -h, --help                     help for kubebuilder
      --plugins strings          plugin keys to be used for this subcommand execution
      --project-version string   project version (default "3")

Use "kubebuilder [command] --help" for more information about a command.

설치가 완료되었으니 이제 샘플 프로그램을 제작해 보자.

 

샘플 Operator

1) 프로젝트 생성

 

먼저 프로젝트를 생성해 보자. 프로젝트는 다음과 같이 만든다.

# mkdir -p ~/projects/echo
# cd ~/projects/echo
# kubebuilder init --domain my.domain --repo my.domain/echo

생성된 파일 리스트는 다음과 같다.

# ls -al
합계 228
drwxr-xr-x. 4 root root   180  8월 11 18:24 .
drwxr-xr-x. 3 root root    23  8월 11 18:23 ..
-rw-------. 1 root root   129  8월 11 18:23 .dockerignore
-rw-------. 1 root root   367  8월 11 18:23 .gitignore
-rw-------. 1 root root   776  8월 11 18:23 Dockerfile
-rw-------. 1 root root  5077  8월 11 18:23 Makefile
-rw-------. 1 root root   111  8월 11 18:24 PROJECT
-rw-------. 1 root root  2721  8월 11 18:23 README.md
drwx------. 6 root root    66  8월 11 18:23 config
-rw-------. 1 root root  3752  8월 11 18:24 go.mod
-rw-r--r--. 1 root root 95177  8월 11 18:24 go.sum
drwx------. 2 root root    32  8월 11 18:23 hack
-rw-------. 1 root root  3483  8월 11 18:23 main.go
  • main.go : 프로젝트의 시작점이다. operator를 설정하고 실행한다.
  • config/ : operator를 배포하기 위한 manifest가 포함되어 있다.
  • Dockerfile : container 이미지를 빌드하는데 사용되는 파일이다.

2) API 생성

 

다음 명령을 실행하여 새 API(그룹/버전) v1과 새 CRD 를 만든다. 

# kubebuilder create api --group app --version v1 --kind Echo

실행 후에 2개의 폴더가 생성되었다.

  • api/v1 : 여기에는 CRD 가 포함되어 있다. ( echo_types.go )
  • controllers : echo 컨트롤러를 포함한다. ( echo_controller.go )

3) CRD 및 Controller 

 

우선 만들고자 하는 CRD의 사양은 다음과 같다.

apiVersion: app.my.domain/v1
kind: Echo
metadata:
  name: echo-sample
spec:
  command: "echo hello world!"

이제 CRD를 구현해 보자. CRD의 구현체는 api/v1/echo_types.go 이다.

 

EchoSpec과 EchoStatus 구조체를 다음과 같이 수정한다.

type EchoSpec 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 Echo. Edit echo_types.go to remove/update
        Command string `json:"command,omitempty"`
}

// EchoStatus defines the observed state of Echo
type EchoStatus struct {
        // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
        // Important: Run "make" to regenerate code after modifying this file
        Phase string `json:"phase,omitempty"`
}

API 정의를 편집하는 경우 다음을 사용하여 사용자 지정 리소스(CR) 또는 사용자 지정 리소스 정의(CRD)와 같은 매니페스트를 생성한다.

# make manifests

K8S 에 CRD를 설치하려면 다음의 명령을 수행한다.

# make install

K8S에서 CRD가 생성되었는지 확인해 보자.

# k get crds
...
echoes.app.my.domain                                  2022-08-11T10:52:27Z
...

이제 한번 CR을 생성해 보자

apiVersion: app.my.domain/v1
kind: Echo
metadata:
  name: echo-sample
spec:
  command: "echo hello world!"

생성 후 조회해 보자.

# k get echo
NAME          AGE
echo-sample   10s

지금까지는 CRD 의 대한 정의를 하였지만, 아직 컨트롤러가 없기 때문에 CR에 대한 어떠한 액션도 없다.

 

이제 컨트롤러 로직을 구현해보자. 컨트롤러는 우리가 생성한 CRD로 무엇을 할 것인지 kubernetes에게 알려주는 기능을 한다. 이번 컨트롤러에서는 CR 생성 시 Pod를 생성한 후 command를 실행해 보는 것을 해 보자.

 

대부분의 scaffolding은 이미 main.go 와 controllers/ 파일안에 있다. 사용자는 Reconcile 함수에 로직을 넣으면 된다고 생각하면 된다.

컨트롤러는 기본적으로 해당 객체에 대해 주기적 루프를 실행하고 해당 객체에 대한 변경 사항을 감시한다. 
변경 사항이 발생하면 루프 본문이 트리거되고 관련 로직이 실행될 수 있다. 이 Reconcile 함수는 모든 컨트롤러 로직의 진입점이다.

 

이미 생성된 controller ( controllers/echo_controller.go ) 의 reconcile 함수를 살펴보자.

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

        // TODO(user): your logic here

        return ctrl.Result{}, nil
}

이제 Pod를 생성하고 command 를 수행하는 로직을 구현해 보자.

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

        var instance appv1.Echo
        errGet := r.Get(ctx, req.NamespacedName, &instance)
        if errGet != nil {
                log.Error(errGet, "Error getting instance")
                return ctrl.Result{}, client.IgnoreNotFound(errGet)
        }

        pod := NewPod(&instance)

        _, errCreate := ctrl.CreateOrUpdate(ctx, r.Client, pod, func() error {
                return ctrl.SetControllerReference(&instance, pod, r.Scheme)
        })

        if errCreate != nil {
                log.Error(errCreate, "Error creating pod")
                return ctrl.Result{}, nil
        }

        err := r.Status().Update(context.TODO(), &instance)
        if err != nil {
                return ctrl.Result{}, err
        }

        return ctrl.Result{}, nil
}

 
func NewPod(cr *appv1.Echo) *corev1.Pod {
        labels := map[string]string{
                "app": cr.Name,
        }

        return &corev1.Pod{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      cr.Name,
                        Namespace: cr.Namespace,
                        Labels:    labels,
                },
                Spec: corev1.PodSpec{
                        Containers: []corev1.Container{
                                {
                                        Name:    "busybox",
                                        Image:   "busybox",
                                        Command: strings.Split(cr.Spec.Command, " "),
                                },
                        },
                        RestartPolicy: corev1.RestartPolicyOnFailure,
                },
        }
}

그리고, SetupWithManager함수에 Owns(&corev1.Pod{}) 를 추가한다. 이것은 컨트롤러 관리자에게 이 컨트롤러에 의해 생성된 포드도 변경 사항을 감시해야 한다고 알려주는 기능이다.

func (r *EchoReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
                For(&appv1.Echo{}).
                Owns(&corev1.Pod{}).
                Complete(r)
}

마지막으로 import 구문에 다음을 추가한다.

import (
        "strings"

        corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
...
)

이제 실행해 보자.

# make run

이렇게 하면 console 에 포그라운드로 실행되면서 로그가 출력된다.

test -s /root/project/echo/bin/controller-gen || GOBIN=/root/project/echo/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/root/project/echo/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/root/project/echo/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
1.6602180333173583e+09  INFO    controller-runtime.metrics      Metrics server is starting to listen    {"addr": ":8080"}
1.6602180333181918e+09  INFO    setup   starting manager
1.6602180333185756e+09  INFO    Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
1.6602180333186262e+09  INFO    Starting EventSource    {"controller": "echo", "controllerGroup": "app.my.domain", "controllerKind": "Echo", "source": "kind source: *v1.Echo"}
1.6602180333186808e+09  INFO    Starting EventSource    {"controller": "echo", "controllerGroup": "app.my.domain", "controllerKind": "Echo", "source": "kind source: *v1.Pod"}
1.6602180333186896e+09  INFO    Starting Controller     {"controller": "echo", "controllerGroup": "app.my.domain", "controllerKind": "Echo"}
1.6602180333187807e+09  INFO    Starting server {"kind": "health probe", "addr": "[::]:8081"}
1.6602180334198806e+09  INFO    Starting workers        {"controller": "echo", "controllerGroup": "app.my.domain", "controllerKind": "Echo", "worker count": 1}

Pod가 실행되었는지 확인해 보자.

# k get po -n default
NAME                              READY   STATUS      RESTARTS   AGE
echo-sample                       0/1     Completed   0          43s

# k logs -f echo-sample
hello world!

 

클러스터에 배포

다음에서 지정한 위치에 이미지를 빌드하고 푸시한다.

# make docker-build docker-push IMG=<some-registry>/<project-name>:tag

다음으로 지정된 이미지를 사용하여 컨트롤러를 클러스터에 배포한다.

# make deploy IMG=<some-registry>/<project-name>:tag

 

CRD 제거

클러스터에서 CRD를 삭제하려면

# make uninstall

 

Controller 배포 취소

배포된 컨트롤러를 취소하려면

# make undeploy
반응형

'개발 > go' 카테고리의 다른 글

GoDS ( Go Data Structures )  (0) 2022.08.24
Viper  (0) 2022.08.23
cobra library 사용법  (0) 2022.08.22
모둘과 패키지  (0) 2022.08.22
Golang - Gin Framework  (0) 2022.08.08
Comments