Kubernetes Operator를 만들어보자 3일차 - Operator SDK

0

Kubernetes Operator

목록 보기
3/5

Operator SDK를 통한 Operator 개발

golang과 operator-sdk을 통해서 개발을 진행해보도록 하자.

project setup

먼저 operator-sdk를 설치해주어야 하는데, brew 사용자라면 다음의 명령어를 통해서 쉽게 설치가 가능하다.

brew install operator-sdk

brew 사용자가 아니라면 직접 github에서 project를 clone하여 golang을 통해 build할 수 있다. 즉, golang이 있어야한다는 것이다. 만약 golang이 있다면 다음의 명령어를 통해서 설치가 가능하다.

sudo GOBIN='/usr/local/bin/'  make install

참고로 GOBIN은 golang으로 빌드된 binary를 저장할 장소를 지정하는 것이다. 우리의 경우 operator-sdk가 빌드되어 binary가 GOBIN에 저장될 것이다.

이제 operator-sdk를 사용하여 project를 빌드해보도록 하자.

PROJECT_NAME="nginx-operator"
mkdir -p $HOME/projects/${PROJECT_NAME}
cd $HOME/projects/${PROJECT_NAME}
# we'll use a domain of example.com
# so all API groups will be <group>.example.com
operator-sdk init --domain example.com --repo github.com/example/${PROJECT_NAME}

원하는 PROJECT_NAME을 적어주면 된다. 우리의 경우 nginx-operator으로 만들기로 하자. 참고로 domain부분은 앞으로 만들 CRD의 API group prefix로 사용된다. 그러니 신중하게 고르길 바란다.

이제 PROJECT_NAME에 우리의 operator boiler plate가 만들어졌을 것이다. 개발에 앞서 operator를 먼저 설계해보도록 하자.

INFO[0000] Writing kustomize manifests for you to edit... 
INFO[0000] Writing scaffold for you to edit...          
INFO[0000] Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.16.3 
INFO[0002] Update dependencies:
$ go mod tidy           
Next: define a resource with:
$ operator-sdk create api

다음의 명령어 이후, 해당 directory에서 ls명령어로 확인해보도록 하자.

~/nginx-operator$ ls
total 112K
drwxr-xr-x   12 mdame staff  384 Dec 22 21:07 .
drwxr-xr-x+ 282 mdame staff 8.9K Dec 22 21:06 ..
drwx------    8 mdame staff  256 Dec 22 21:07 config
drwx------    3 mdame staff   96 Dec 22 21:06 hack
-rw-------    1 mdame staff  129 Dec 22 21:06 .dockerignore
-rw-------    1 mdame staff  367 Dec 22 21:06 .gitignore
-rw-------    1 mdame staff  776 Dec 22 21:06 Dockerfile
-rw-------    1 mdame staff 8.7K Dec 22 21:07 Makefile
-rw-------    1 mdame staff  228 Dec 22 21:07 PROJECT
-rw-------    1 mdame staff  157 Dec 22 21:07 go.mod
-rw-r--r--    1 mdame staff  76K Dec 22 21:07 go.sum
-rw-------    1 mdame staff 2.8K Dec 22 21:06 main.go

성공적으로 설치된 것을 볼 수 있다.

각 directory를 소개하자면 다음과 같다.

  1. config: operator resource에 관한 yaml definition들을 가지고 있다.
  2. hack: 여러 hack script들을 가지기 위해 사용된다. 이 script들은 여러 목적을 위해서 사용되는데, 대게 변화를 만들거나 검증하기위해서 사용된다.
  3. PROJECT: kubebuilder에 의해 사용되는 file로 project config 정보를 담기위해서 사용된다.

operator에 대한 template를 만들었지만, 내용은 완전 텅 비어있기 때문에 비어있는 controller가 실행된다. 이는 ReadyzHealthz endpoint들만 가지고 실행되고 있는 것이다.

API 정의

operator의 API는 kubernetes cluster에서 어떻게 operator가 동작할 것인지를 정의하는 것이다. 이 API들은 operator와 상호작용할 Custom Resource Object에 대한 blueprint를 제공하는 CRD로 해석된다. 그러므로 이 API를 만드는 것은 가장 중요한 일인 것이다. API없이는 Operator의 logic code가 Custom Resource로부터 value를 읽어 올 방법이 없다.

Operator API는 go 구조체로 구현되어 object를 나타낸다. 이 구조체에 대한 기본적인 정의를 operator-sdk가 도와주는데, 다음의 명령어를 실행해보도록 하자.

CGO_ENABLED=0 operator-sdk create api --group operator --version v1alpha1 --kind NginxOperator --resource --controller

주의!! 현재 golang version이 1.22 이상이면 아래의 error가 발생한다.

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xa0a1af]
...
Global Flags:
      --plugins strings   plugin keys to be used for this subcommand execution
      --verbose           Enable verbose logging

FATA[0002] failed to create API: unable to run post-scaffold tasks of "base.go.kubebuilder.io/v4": exit status 2 

golang version이 1.22라면 1.21로 낮추도록 하자. https://anewhope.tistory.com/entry/Go-%EB%B2%84%EC%A0%84-%EB%B3%80%EA%B2%BD-%ED%95%98%EA%B8%B0

위의 command를 통해서 다음이 만들어지게 된 것이다.

  1. API type들을 api/라고 불리는 directory에 생성
  2. 이 type들은 API group이 operator.example.com으로 되어있다. 이는 우리가 operator를 실행할 때 example.com domain으로 잡았기 때문이다.
  3. API의 초기 버전 이름은 v1alpha1이다.
  4. 해당 type의 operator 이름은 NginxOperator이다.
  5. controllers/ directory아래에 boilerplate controller code가 있다. 앞으로 여기에서 작업이 진행될 것이다.
  6. main.go에서 새로운 controller를 실행시키는 boilerplate code가 추가된다.

api/v1alpha1/ directory를 보면 nginxoperator_types.go 파일 이외에 두 개의 file로 groupversion_info.gozz_generated.deepcopy.go이 있다. 이 둘은 직접 수정하지 말아야 하는데, system이 사용하는 file이기 때문이다.

nginxoperator_types.go file을 보면 내부 field가 비어있는 구조체들을 볼 수 있다. 이 file에서 가장 주목해야할 3가지 type들은 다음과 같다.

// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec 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 NginxOperator. Edit nginxoperator_types.go to remove/update
	Foo string `json:"foo,omitempty"`
}

// NginxOperatorStatus defines the observed state of NginxOperator
type NginxOperatorStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// NginxOperator is the Schema for the nginxoperators API
type NginxOperator struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   NginxOperatorSpec   `json:"spec,omitempty"`
	Status NginxOperatorStatus `json:"status,omitempty"`
}

kubernetes API object들은 모두 SpecStatus field를 가진다. 이는 operator 역시도 마찬가지이다. 이 두가지 feild를 활용하여 user input을 받아들이고, operator의 status를 보고하게 된다. 따라서, 다음과 같은 구조를 갖게 되는 것이다.

        ---------------
        |NginxOperator|
        ---------------
                |
    --------------------------------
    |                              |
-------------------     ---------------------
|NginxOperatorSpec|     |NginxOperatorStatus|
-------------------     ---------------------
    |                              |
    --------------------------------
                |
          --------------
          |Control Loop|
          --------------

Spec부분에 operator CR 구동에 필요한 option들을 추가하면 된다. 우리의 경우는 operator의 operand로 nginx를 구동시킬 것이기 때문에 다음의 option들을 넣도록 하자.

  1. port: nginx pod가 open할 port
  2. replicas: operator를 통해서 deployment를 만들어 nginx pod를 배포할 때, 필요한 replica의 수
  3. forceRedeploy: 이는 no-operator(no-op) field로 operator가 nginx operand를 재배포하도록 지시하는 field이다.

위의 option들을 우리의 operator spec에 추가해주도록 하자. 단, 각 type들에 대해서 pointer형으로 만드는 것에 주의하도록 하자. pointer형으로 만들어서 golang에서의 zero value와 nil값을 구분하기 위한 것이다. 즉, default값과 입력 조차 안들어온 값을 구분하기 위함이다.

// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
	// Port is the port number to expose on the Nginx Pod
	Port *int32 `json:"port,omitempty"`

	// Replicas is the number of deployment replicas to scale
	Replicas *int32 `json:"replicas,omitempty"`

	// ForceRedploy is any string, modifying this field
	// instructs  the Operator to redeploy the Operand
	ForceRedeploy string `json:"forceRedeploy,omitempty"`
}

이렇게 operator types에 관해서 수정이 발생하면 make generate 명령어를 사용해주는 것이 좋다. 이 명령어를 사용하면, 처음에 생성된 file들을 수정해주는데, 수정의 대상이 되는 것이 바로 위에서 언급했던 zz_generated.deepcopy.go이다. 따라서, CI system을 개발할 때 강제로 make generate를 실행시켜주도록 하는 것이 좋다.

각 field에는 json으로 tag가 붙어있는데 json으로 형식을 변환할 때 사용하는 golang 문법이다. Port는 json으로 port로 변환되고 Replicasreplicas로 변환되는 것이다. omitempty는 만약 해당 값이 비어있다면, json key로 쓰지 않겠다는 것을 나타낸다. 따라서, 이는 optional을 의미하는 것이다.

만약 특정 field를 optional이 아니라 required로 바꾸고 싶다면 omitempty를 없애면 된다.

   // Port is the port number to expose on the Nginx Pod
   // +kubebuilder:default=8080
   // +kubebuilder:validation:Required
   Port int `json:"port"`

resource manifests 추가

이제 우리의 operator golang struct를 기반으로 CRD manifest를 만들 수 있다. 다음의 명령어를 사용하도록 하자.

make manifests

해당 명령어를 사용하면 API에 기반으로 CRD를 생성한다. 해당 CRD는 config/crd/bases/operator.example.com_nginxoperators.yaml 아래에 놓여있게 된다. 다음을 살펴보도록 하자.

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.13.0
  name: nginxoperators.operator.example.com
spec:
  group: operator.example.com
  names:
    kind: NginxOperator
    listKind: NginxOperatorList
    plural: nginxoperators
    singular: nginxoperator
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: NginxOperator is the Schema for the nginxoperators API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: NginxOperatorSpec defines the desired state of NginxOperator
            properties:
              forceRedeploy:
                description: ForceRedploy is any string, modifying this field instructs  the
                  Operator to redeploy the Operand
                type: string
              port:
                description: Port is the port number to expose on the Nginx Pod
                format: int32
                type: integer
              replicas:
                description: Replicas is the number of deployment replicas to scale
                format: int32
                type: integer
            type: object
          status:
            description: NginxOperatorStatus defines the observed state of NginxOperator
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}

아주 이쁘게 만들어진 것으 볼 수 있다. 심지어는 operator의 spec부분에 정의한 구조체 field의 주석을 토대로 description을 추가한 것을 볼 수 있다.

make manifests는 또한 operator 구동에 필요한 관련 resource들을 생성해주는데, 대표적으로 Role-Based Access Control(RBAC)를 만들어준다. 이를 기반으로 operator가 kubernetes apiserver와 상호작용 할 수 있는 것이다.

해당 file은 config/rbac/role.yaml을 확인하면 나온다.

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators/finalizers
  verbs:
  - update
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators/status
  verbs:
  - get
  - patch
  - update

kubernetes apiserver에 대한 요청 권한 role들이 들어있는데, 안쓸 것들은 빼는 것이 좋다. 그러나, 아마 대부분 귀찮아서 그냥 이대로 쓴다.

추가적인 manifests와 BinData

나머지 남은 resource들은 ClusterRole과 nginx pod를 구동시킬 Deployment이다. ClusterRolekubebuild tags를 통해서 code로 생성할 수 있다. 이에 대해서는 control loop를 만드는 section에서 다루기로 하자. 먼저 Deployment를 정의하여 control loop에서 접근할 수 있도록 하자.

이전에도 말했지만, kubernetes client library를 사용하여 deployment와 같은 kubernetes object를 만드는 방법은 유지 보수 방법에서 그렇게 좋지가 않다. yaml 파일 그대로를 들고 있는 것이 좋다. 이를 위해서 yaml 파일을 가지고 있을 assets directory를 만들고, manifests directory를 만든 다음 아래의 yaml을 넣도록 하자.

  • assets/manifests/nginx_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: "nginx-deployment"
  namespace: "nginx-operator-ns"
  labels:
    app: "nginx"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: "nginx"
  template:
    metadata:
      labels:
        app: "nginx"
    spec:
      containers:
        - name: "nginx"
          image: "nginx:latest"
          ports:
          - containerPort: 80

문제는 이 file을 어떻게 operator에게 전달할 것인가이다. 다행히도 go 1.16이후부터는 go:embed라는 것이 추가되어 빌드된 golang binary에 file을 추가할 수 있다.

먼저 assets directory에 assets.go 파일을 만들도록 하자. 구조가 다음과 같아진다.

./nginx-operator/
| - assets/
| - - assets.go
| - - manifests/
| - - - nginx_deployment.yaml

assets.go에 다음의 code를 넣도록 하자. go:embed를 사용하는 방법은 매우 간단한데, 주석으로 //go:embed {filepath}를 써주면 된다. 그리고 해당 file을 받을 변수를 정해주면 된다.

  • assets/assets.go
package assets

import (
	"embed"

	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
	//go:embed manifests/*
	manifests  embed.FS
	appsScheme = runtime.NewScheme()
	appsCodecs = serializer.NewCodecFactory(appsScheme)
)

func init() {
	if err := appsv1.AddToScheme(appsScheme); err != nil {
		panic(err)
	}
}
func GetDeploymentFromFile(name string) *appsv1.Deployment {
	deploymentBytes, err := manifests.ReadFile(name)
	if err != nil {
		panic(err)
	}
	deploymentObject, err := runtime.Decode(
		appsCodecs.UniversalDecoder(appsv1.SchemeGroupVersion),
		deploymentBytes,
	)
	if err != nil {
		panic(err)
	}
	return deploymentObject.(*appsv1.Deployment)
}

//go:embed manifests/* 주석을 통해서 compiler는 manifests directory에 있는 file들을 manifests 변수에 넣어준다. 이제 manifests를 통해서 file을 가져오기만 하면 된다. 이 부분을 편리하게 하기위해서 GetDeploymentFromFile 함수를 만들었다.

이제 deployment yaml에 대한 정보를 가져올 때는 다음과 같이 사용하기만 하면 된다.

import "github.com/sample/nginx-operator/assets"

...

nginxDeployment := assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")

이제 control loop를 동작시키기에 충분하다.

개인적으로 go:embed 방식으로 manifests 파일들을 관리하는 것을 선호하진 않는다. 그냥 volume을 통해 yaml파일을 받아서 sprig로 template parsing을 해주는 것이 더 좋지 않을까 생각한다. 이유는 동적으로 file들을 변경하고 수정할 수 있기 때문이다.

Control loop 개발

operator의 control loop는 특정한 cluster event가 발생하면 트리거되어 reconciliation function이 실행된다. 즉, loop형식으로 계속 반복되는 것이 아니라, operator의 main thread가 관련된 event들이 발생한 지 cluster를 지속적으로 관찰하여 발생했다면 그때야 reconciliation function을 실행시키는 것이다.

Reconcilie 함수는 이미 operator-sdk에 의해서 구조화되어 있어서 internal/controller/nginxoperator_controller.go에 있다.

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

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

Reconcile의 반환값으로 ctrl.Resulterror를 받는데, 위의 코드에서는 빈 ctrl.Result{}nil error를 반환하고 있다. 이렇게 빈 ctr.Result{}를 반환하거나 error가 nil이라면 문제가 없는 것으로 받아들여져 Reconcile은 정상 종료된다.

그러나, 만약에 Reconcile에 error가 nil이 아니거나 ctrl.Result{}에 뭔가 정보가 있다면 Reconcile 정상 종료되지 않았다는 것을 의미하므로, Reconcile이 재시도된다.

이제 code를 추가해보도록 하자. r.Get 함수는 주어진 namespace에서 해당 kubernetes object가 있는 지 없는 지 확인하는 함수이다. 따라서, 우리의 operator CR를 넘겨주면 operator object가 배포되었는 지 아닌 지 확인하여 결과를 반환해준다. 만약 배포되어있다면 성공이라고 생각하고, 없다면 error를 반환하도록 code를 만들어보도록 하자.

  • internal/controller/nginxoperator_controller.go
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	operator := &operatorv1alpha1.NginxOperator{}

	err := r.Get(ctx, req.NamespacedName, operator)
	if err != nil && errors.IsNotFound(err) {
		logger.Info("Operator resource object not found.")
		return ctrl.Result{}, nil
	} else if err != nil {
		logger.Error(err, "Error getting operator resource object")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

errorsk8s.io/apimachinery/pkg/api/errors에서 제공하는 package로 kubernetes 상에서 발생하는 error들에 대한 wrapper이다. 이를 활용하여 error를 구체화하고 있는데, errors.IsNotFound(err)err의 내용이 해당 object를 찾지 못했을 때 발생하는 error를 특정하여 조건문에 넣은 것이다.

이는 정교하게 kubernetes cluster를 다룰 수 있는 장점이 있는데, 가령 잠시 operator CR이 재시작되어 다시 올라가고 있는 중에, 해당 Get이 실행되면 IsNotFound error의 대상이 아니기 때문에 조건을 넘어간다.

req paramater는 reconciliation를 트리거링하는 event를 발생시킨 object의 name과 namespace를 가지고 있다. req.NamespacedName은 해당 object에 대한 namenamespace를 담고 있는 구조체로 보면 된다. 단, operator와 같은 namespace로 배포되는 것을 잊지말도록 하자.

operator CR을 얻을 수 있다는 것은 operator spec부분에 접근이 가능하다는 것이다. 이제 spec을 기반으로 operand에 설정을 해주어야하는데, 우리의 경우 nginx deployment를 만드려고 하는 것이다. 이를 위해서 deployment가 먼저 있는 지 확인해보도록 하자. 만약, 없다면 위에서 설정한 nginx_deployment.yaml파일을 가져와서 spec부분에 맞게 data를 설정해주기만 하면 된다.

  • internal/controller/nginxoperator_controller.go
package controller

import (
	appsv1 "k8s.io/api/apps/v1"
	...
	ctrl "sigs.k8s.io/controller-runtime"
	...
	operatorv1alpha1 "github.com/example/nginx-operator/api/v1alpha1"
)
...
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	operatorCR := &operatorv1alpha1.NginxOperator{}

	err := r.Get(ctx, req.NamespacedName, operatorCR)
	if err != nil && errors.IsNotFound(err) {
		logger.Info("Operator resource object not found.")
		return ctrl.Result{}, nil
	} else if err != nil {
		logger.Error(err, "Error getting operator resource object")
		return ctrl.Result{}, err
	}

	deployment := &appsv1.Deployment{}
	err = r.Get(ctx, req.NamespacedName, deployment)
	if err != nil && errors.IsNotFound(err) {
		deployment.Namespace = req.Namespace
		deployment.Name = req.Name
		deploymentManifest := assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")
		deploymentManifest.Spec.Replicas = operatorCR.Spec.Replicas
		deploymentManifest.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port

		err = r.Create(ctx, deploymentManifest)
		if err != nil {
			logger.Error(err, "Error creating Nginx deployment")
			return ctrl.Result{}, err
		}

		return ctrl.Result{}, nil
	} else if err != nil {
		logger.Error(err, "Error getting existing Nginx deployment")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

assets.GetDeploymentFromFile()을 호출하여 deploymentManifest를 가져오고 여기의 property값들을 수정하도록 한다.

참고로 만약 Reconcile을 한번 더 실행하고 싶다면 ctrl.Result{Requeue: true}를 설정하면 된다.

그런데, 이렇게 만들면 이미 deployment가 있을 때 deployment에 대한 update가 불가능하다. 즉, create는 가능하지만 update는 안되는 것이다. 어차피 createupdate든 로직은 같으므로 다음과 같이 공통화된 부분을 묶어 code를 만들 수 있다.

  • internal/controller/nginxoperator_controller.go
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	operatorCR := &operatorv1alpha1.NginxOperator{}

	err := r.Get(ctx, req.NamespacedName, operatorCR)
	if err != nil && errors.IsNotFound(err) {
		logger.Info("Operator resource object not found.")
		return ctrl.Result{}, nil
	} else if err != nil {
		logger.Error(err, "Error getting operator resource object")
		return ctrl.Result{}, err
	}

	deployment := &appsv1.Deployment{}
	create := false
	err = r.Get(ctx, req.NamespacedName, deployment)
	if err != nil && errors.IsNotFound(err) {
		create = true
		deployment = assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")
	} else if err != nil {
		logger.Error(err, "Error getting existing Nginx deployment.")
		return ctrl.Result{}, err
	}

	deployment.Namespace = req.Namespace
	deployment.Name = req.Name
	if operatorCR.Spec.Replicas != nil {
		deployment.Spec.Replicas = operatorCR.Spec.Replicas
	}
	if operatorCR.Spec.Port != nil {
		deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
	}
	ctrl.SetControllerReference(operatorCR, deployment, r.Scheme)

	if create {
		err = r.Create(ctx, deployment)
	} else {
		err = r.Update(ctx, deployment)
	}

	return ctrl.Result{}, err
}

createupdate에 대한 공용화 code부분은 위로 보내고, r.Create, r.Update code만 아래로 보내어 분기 처리해주었다. code가 더 깔끔해진 것을 볼 수 있다.

ctrl.SetControllerReference()은 해당 kubernetes object가 누구에게 권한이 있는 지에 대한 것을 나타낸다. 우리의 경우 nginx operator로부터 만들어진 deployment라는 것을 표시하는 것이다. object에 OwnerReference로 표시되게되고, API field에서는 owns로 지정된다. 이렇게 설정해두면 garbage collection에 좋다.

이제 operator를 구현했으므로, operator에게 권한을 부여해주어야 한다. 즉, deployment에 대한 get, create, update 권한을 주어야 한다는 것이다. 이를 위해서 cluster에 대한 RBAC role을 업데이트할 필요가 있다.

그러나, 하나하나 이것을 만드는 일은 매우 번거로운 일인데 KubeBuilder를 사용면 Reconcile()함수에 있는 marker를 이용해, Role을 작동으로 만들 수 있다. 여기서 marker라는 것은 주석으로 Reconcile 함수에 이미 다음의 marker가 작성되어 있을 것이다.

//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators/finalizers,verbs=update

여기에 추가로 다음의 marker를 넣어서 deployment에 대한 권한을 부여해주도록 하자.

//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

make manifests 명령어를 실행하면 Operator의 ClusterRoleconfig/rbac/role.yaml에 만들어질 것이다.

  • config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators/finalizers
  verbs:
  - update
- apiGroups:
  - operator.example.com
  resources:
  - nginxoperators/status
  verbs:
  - get
  - patch
  - update

이제 기본적인 control loop를 만들었다. 그런데, 어떤 event들이 이 loop를 트리거하는가? 이는 SetupWithManager()로 설정할 수 있다.

  • internal/controller/nginxoperator_controller.go
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&operatorv1alpha1.NginxOperator{}).
  Complete(r)
}

해당 code는 cluster에서 NginxOperator에 관한 변화를 관찰하기 위해서 생성되었다. 그러나 우리는 Deployment object에 대한 변화 역시도 관찰해야한다. 따라서, 함수를 다음과 같이 변경하기로 하자.

  • internal/controller/nginxoperator_controller.go
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&operatorv1alpha1.NginxOperator{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

Owns(&appsv1.Deployment{})을 추가했기 때문에 controller manager는 이제, 이전에 SetControllerReference으로 마킹했던 nginx deployment에 대해서 event가 발생하면 control loop로 reconcile을 동작하게 되는 것이다.

재밌는 것은 SetControllerReference로 특정 object에 owner를 등록하는 것은 list형식이기 때문에 여러 owner가 등록할 수 있다. 따라서, 다른 operator 역시도 이 deployment에 대해서 소유자로 마킹만 한다면 Owns를 호출할 수 있다.

이렇게 기본적인 operator 구현이 완료된 것이다.

Operator 배포와 종료

배포하기 전에 docker를 통해서 operator를 구동시킬 수 있도록 하는 operator-controller-manager image를 빌드해야한다. operator sdk에서는 이를 위한 makefile script를 지원하므로 다음의 명령어를 통해서 docker image를 빌드하도록 하자.

sudo make docker-build

image가 잘 빌드되었는 지 docker images로 확인해보도록 하자. 만약 docker image의 이름이 마음에 들지 않다면, /config/manager/manager.yaml 파일에서 image 명을 수정하면 된다.

다음으로 operator manifest를 만들도록 하자. /config/samples 아래를 보면 아마 operator_v1alpha1_nginxoperator.yaml 파일이 있을 것이다. 여기에 우리가 operator CRD에 정의한 spec부분 data를 넣어주도록 하자.

  • operator_v1alpha1_nginxoperator.yaml
apiVersion: operator.example.com/v1alpha1
kind: NginxOperator
metadata:
  labels:
    app.kubernetes.io/name: nginxoperator
    app.kubernetes.io/instance: nginxoperator-sample
    app.kubernetes.io/part-of: nginx-operator
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: nginx-operator
  name: nginxoperator-sample
  namespace: nginx-operator-system
spec:
  port: 8082
  replicas: 3

이제 kubernetes cluster에 operator-controller-manager를 배포시켜보도록 하자.

sudo make deploy

잘 배포되었다면 다음의 결과가 나올 것이다.

kubectl get po -n nginx-operator-system
NAME                                                 READY   STATUS    RESTARTS   AGE
nginx-operator-controller-manager-69b8fccc98-stbrr   2/2     Running   0          41m

우리의 Operator CRD가 kubernetes cluster에 적용된 것이다. 이제 operator manifest를 배포해주도록 하자.

sudo kubectl create -f ./config/samples/operator_v1alpha1_nginxoperator.yaml

replicas를 3으로 설정했기 때문에 operator가 배포되고나서 3개의 pod가 올라와야 한다.

kubectl get po -n nginx-operator-system
NAME                                                 READY   STATUS    RESTARTS   AGE
nginx-operator-controller-manager-69b8fccc98-stbrr   2/2     Running   0          41m
nginxoperator-sample-6899cc8684-4h4pg                1/1     Running   0          41m
nginxoperator-sample-6899cc8684-jbtvb                1/1     Running   0          41m
nginxoperator-sample-6899cc8684-x6sm9                1/1     Running   0          41m

성공한 것을 볼 수 있다.

만약 operand를 내리고 싶다면 operator를 삭제하면 된다.

sudo kubectl delete -f ./config/samples/operator_v1alpha1_nginxoperator.yaml 

마지막으로 operator-controller-manager를 삭제하고 싶다면 다음의 명령어를 실행시키면 된다.

sudo make undeploy

0개의 댓글