Kubernetes/Kubernetes 이론

Custom Resource Definition (CRD) Golang

이쿠우우 2020. 11. 5. 20:19
반응형

 

 

 

 

 

Custom Resource Definition (CRD) Golang


[Kubernetes Custom Resource 관련글 목록]

Kubernetes Version 1.18 에 해당함.

Kubernetes Custom Resource 개념

Custion Resource Definition (CRD) Python

Custom Resource Definition (CRD) GoLang


 

 

[목표]

iksoon.test.com CRD를 생성 시 

choice 옵션에 mysql 또는 tomcat를 선택하면

해당 image Pod가 생성되고 

Service도 NodePort Type으로 자동생성되도록 함.

choice 옵션에 mysql, tomcat 이외에 값이 올 경우 error 처리함.

 

[환경]

 

Master Node server

 OS = CentOS 7

 리눅스 커널 버전 : Linux 3.10.0-1062.el7.x86_64

 docker version : 1.13.1

 api verison : 1.26

 

Worker Node server

 OS = CentOS 7

 리눅스 커널 버전 : Linux 3.10.0-1062.el7.x86_64

 docker version : 1.13.1

 api verison : 1.26

 

Kubernetes version

 1.18

 

 


 

 

 

1. Custom Resource Definition (CRD) 생성

 

1.1. 예제 crd.yaml 

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # name : 아래의 spec 필드 설정과 동일해야함. spec의 plural와 group를 조합한 값이와야 함. <plural>.<group>
  name: crdplural.iksoon.test.com
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: iksoon.test.com
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: crdplural
    # singular name to be used as an alias on the CLI and for display
    singular: crdsingular
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: CrdKind
    # shortNames allow shorter string to match your resource on the CLI
    shortNames:
    - crdshort
    - iksoontest
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      # Each version can be enabled/disabled by Served flag.
      served: true
      # One and only one version must be marked as the storage version.
      storage: true
      schema:
      # openAPIV3Schema is the schema for validating custom objects.
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                choice:
                  type: string
                  pattern: (mysql|tomcat)
                replicas:
                  type: integer
                  minimum: 1
                  maximum: 10
      additionalPrinterColumns:
        - name: DBorWAS
          type: string
          priority: 0
          jsonPath: .spec.choice
          description: Choose Mysql or Tomcat
        - name: replicas
          type: string
          priority: 1
          jsonPath: .spec.replicas
          description: The type of the database
        - name: Age
          type: date
          jsonPath: .metadata.creationTimestamp

 

 

 

1.2. CRD 생성

 

[명령어]

kubectl apply -f crd.yaml

 

[생성 확인 명령어]

kubectl get crd

 

 

 

 

2. CRD에 해당는 CrdKind 생성해보기

 

2.1. 예제 test.yaml 

apiVersion: "iksoon.test.com/v1"
kind: CrdKind
metadata:
  name: iksoon-crd-test
spec:
  choice: mysql
  replicas: 1

spec 하위에 설정되는 옵션은 생략해도 create는 성공하지만

없는 옵션이 추가될 수는 없음.

 

 

2.2. 생성

[명령어]

kubectl apply -f test.yaml

 

 

 

2.3. 생성 확인

상위의 plural, singular short 에 설정된 값으로 조회 가능.

-o wide 옵션을 사용하면

상위에서 additionalPrinterColumns 설정의 priority 값이 1로 설정된

REPLICAS 설정이 보임

 

2.4. 결과

 

상위 설정이 반영되서

choice 설정이 mysql 또는 tomcat 으로 설정된 경우만 정상적으로 생성되고

이외의 값이 올 경우에는 오류가 발생하면서 생성이 안됨.

 

[참고 mysql, tomcat이외의 값으로 설정된 경우]

하지만 이 단계까지 완료해서 crd 생성까지 하고 해당 내용도 조회가 되지만

실제적으로 pod가 생성되거나 service가 생성되진 않음.

그저 kube-ectd 에 해당 data를 저장할 뿐임.

다른 모듈이 etcd 에 생성된 crd data를 확인하고 

pod 또는 service를 생성하는 등 정해진 임무를 수행해야하는데

아직 custom controller가 없어서 etcd에 data가 생성되어도 특별한 동작을 하지 않음.

 

이제 해당 crd 에 해당하는 custom controller 를 생성해서 

목표로 하는 pod 와 service가 생성되도록 해야함.

 

 

 


 

 

 

Custom Controller 생성

 

[사용 개발 도구]

https://github.com/kubernetes/client-go

 

 

1. Golang 설치

 

 

[Golang yum 설치 명령어]

yum install -y golang

 

[Test환경 Golang 환경변수 설정 예시]

vi ~/.bash_profile

저장 후

source ~/.bash_profile

 

예제 환경 GOPATH를 

GOPATH=/root/k8s/go_test/go_crd

로 설정함.

 

 

 

2. Code Generator 

 

2.1. Code Generator 란?

 

Kubernetes 에서는 

kubectl를 통해 kube-apiserver로 각종 자원 create, delete, update등 이벤트 명령을 전달하면 

이를 탐지하고 해당 request를 확인할 수 있는 라이브러리를 제공하고 있음.

 

GoLang에서는 이러한 기능을 제공하는 라이브러리로

k8s.io/client-go/kubernetes 가 있음.

 

해당 라이브러리를 사용하면 deployment, replicaset, daemonset 등

kubernetes에서 기본적으로 제공하고 있는 core resource의 create, delete, update등 이벤트 변화를 

탐지할 수 있음.

 

이렇게 core resource의 경우에는 k8s.io/client-go/kubernetes 라이브러리를 사용하면 되지만

core resource가 아닌 custom resource의 경우는 k8s.io/client-go/kubernetes 라이브러리로 탐지가 되지 않았음.

즉 custom resource 이벤트를 탐지하지 못하니 custom controller를 생성할 수 가 없었음.

이를 해결하기 위해 custom rescoure 이벤트를 탐지하는 방법을 리서치 했어야했음.

 

그러던 중 찾은것이 바로 Code Genterator 임.

 

[ 참고 : Core Resource ]

kubernetes에서 기본적으로 제공하고 있는 자원을 뜻함.

해당 Core Resource에 해당하는 controller는 

kubernetes cluster를 구성하고 있는 kube-controller 모듈임.

 

 

Code Genterator 자체가 custom controller는 아니고

이전 글인 python custom resource 실습에서 설명했던

kopf 라이브러리와 동일한 역할에 해당함.

즉 이벤트 탐지에 해당.

 

Code Genterator에는

Informer와 Client라는 개념이 사용됨

해당 개념에 대해서 아주 깊게 리서치를 진행하진 않았음으로

간단하게 역할에 대해서만 설명하겠음

 

2.2. Informer

informer는 Kubernetes Cluster와 Controller 사이에서 중개자 역할을 함.

informer는 특정 Resource 타입에 대해 클러스터에서 변화가 생길 때마다 

해당 이벤트를 들을 수 있는 이벤트 핸들러를 등록할 수 있다고 함.

[자세한 설명 참고]

https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html

https://getoutsidedoor.com/2020/05/09/kubernetes-controller-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0/

 

2.3. Client

kubernetes cluster에 접근하려면, cluster 위치를 알아야 하고 

접근할 수 있는 자격 증명이 있어야함.

Client란 GoLang에서 위의 역할을 수행하는 것을 말함.

 

[자세한 설명 참고]

https://kubernetes.io/ko/docs/tasks/administer-cluster/access-cluster-api/

 

 

그림 참고 : https://insujang.github.io/2020-02-13/programming-kubernetes-crd/

 

 

2.3. Code Generator 역할 

code generator는 개발자가 CRD에 대한 기본적인 자료구조를 생성만 해놓으면

자동으로 CRD에 대해서 informer와 client를 생성해주는 tool임.

 

[code generator 사용 절차]

1. 적절한 code generator 주석 tag로 기본적인 자료구조 생성.

2. code generator를 실행하여 고객 리소스에 대한 clientset, informers, listers를 포함하는 client 코드를 자동으로 생성함.

 

 

 

 

3. Code Generator 사용을 위한 프로젝트 Directory 구조

 

[예제 환경 Directory 생성]

/root/k8s/go_test/go_crd/pkg/apis 경로에 Informer 를 정의할것임

 

/root/k8s/go_test/go_crd/pkg/apis/iksooncrd

iksooncrd : custom resource name. 상위에서 생성한 CRD와 호환되는 건 없으니 구별하기 쉬운 name으로 설정.

 

/root/k8s/go_test/go_crd/cmd/main.go

예제로 해당 경로에 main 코드를 작성함.

 

/root/k8s/go_test/go_crd/src

해당 경로에 code generator 등 필요한 라이브러리를 저장할 것임.

 

 

[참고]

pkg와 src Directory name은 고정.

 

 

 

 

 

4. Code Generator Download

[명령어]

cd /root/k8s/go_test/go_crd/src

go get k8s.io/code-generator

 

추가로 필요한 apimachinery 라이브러리도 download함

go get k8s.io/apimachinery

 

 

 

 

5. Custom Resource Definition 기본 자료구조 생성

 

5.1. pkg/apis/iksooncrd/register.go

 

생성할 custom resource 의 Group 명을 명시해줌

package iksooncrdtest
const (
        GroupName = "iksoon.test.com"
)

 

 

 

5.2. pkg/apis/iksooncrd/v1/doc.go

 

주석부분이 중요함.

code generator 는 아래 예제의 주석을 보고 client package를 생성함.

deepcopy 를 수행하라는 주석으로 

package를 명시하고 

생성할 custom resource 의 Group 명을 명시해줌

주석에 설정된 Package에 존재하는 모든 코드에 영향을 줌

// +k8s:deepcopy-gen=package
// +groupName=iksoon.test.com


package v1

 

 

 

5.3. pkg/apis/iksooncrd/v1/register.go

 

Custom Resource Definition의 version.schema.openAPIV3Schema 에 설정했던

설정을 정의하는 코드.

AddToscheme와 Resource를 정의해줘야함.

package v1
import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/runtime/schema"


        iksoon "../../../../pkg/apis/iksooncrd"
)




// GroupVersion is the identifier for the API which includes
// the name of the group and the version of the API
var SchemeGroupVersion = schema.GroupVersion{
        Group:   iksoon.GroupName,
        Version: "v1",
}




// AddToScheme creates a SchemeBuilder which uses functions to add types
// to the scheme
var (
        SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
        AddToScheme   = SchemeBuilder.AddToScheme
)




func Resource(resource string) schema.GroupResource {
        return SchemeGroupVersion.WithResource(resource).GroupResource()
}




func addKnownTypes(scheme *runtime.Scheme) error {
        scheme.AddKnownTypes(
                SchemeGroupVersion,
                &CrdKind{},
                &CrdKindList{},
        )
        metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
        return nil
}



 

 

 

5.4. pkg/apis/iksooncrd/v1/types.go

 

Custom Resource Definition에 해당하는 자료구조를 정의하는 코드.

마찬가지로 code generator 는 아래 예제의 주석을 보고 client package를 생성함.

 

[주석에 대한 설명]

( +genclient: )

현재 패키지에 정의된 자료 구조에 대해서 Kubernetes Client를 생성.

사용자는 해당 client를 통해 Kubernetes 리소스 이벤트를 확인할 수 있음.

( +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object: ) 

다음 코드에 대해서 deep copy 로직을 생성함.

 

[ type CrdKind struct ]

kubernetes cluster에 생성할 수 있는 자원임을 명시하기 위해

고정값으로 inline과 metadata를 추가하고

CRD에 해당하는 자원을 추가하기 위해 Spec를 명시하고

Spec에 CRD에서 설정한 spec값을 넣어줌.

package v1


import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)




// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type CrdKind struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata"`
        Spec              CrdSpec   `json:"spec,omitempty"`
}




type CrdSpec struct {
        Choice       string `json:"choice"`
        Replicas     *int32 `json:"replicas"`
}




// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type CrdKindList struct {
        metav1.TypeMeta `json:",inline"`
        metav1.ListMeta `son:"metadata,omitempty"`
        Items           []*CrdKind `json:"crdkinds"`
}

 

 

 

6. code generator를 사용해서 CRD자료 구조를 읽어서 Client, informer package 생성

 

[명령어]

~/code-generator 경로에서 실행

./generate-groups.sh all  ../pkg/client  ../../../pkg/apis iksooncrd:v1

 

6.1. 명령어 설명

[all]

the generators comma separated to run (deepcopy,defaulter,client,lister,informer)

즉 code generator의 모든 작업을 수행함.

 

[ ../pkg/client ]

Informer, Client Package 생성 경로 설정

경로설정 :  $GOPATH/src 상태경로로 설정

 

[ ../../../pkg/apis ]

상위에서 생성해놨던 CRD 자료구조 경로 설정

pkg/apis 경로를 알려줘야함.

deep copy 함수가 생성됨.

생성된 deep copy 함수 파일명 : zz_generated.deepcopy.go

 

(해당 설정 주의 사항)

리서치를 하던 도중 발견한 증상인데

다른 사이트의 예제를 보면 

해당 설정 경로를 모두 github.com 경로로 설정했는데

리서치를 하면서 github을 사용하고 있지 않아서 상대경로로 설정하던 중 확인한 특이사항임.

해당 설정은 결과적으로 Informer 경로 , deep copy 파일 생성

2가지 설정에 해당하는 부분인데

설정이 반영되는 경로가 다름.

 

CRD 자료구조 경로설정 : 명령어 실행 경로에서 상대경로로 설정

deep copy 파일 생성 경로설정$GOPATH/src 상태경로로 설정

 

하지만 설정을 CRD 자료구조 경로설정에 해당하는 경로로 설정하는 이유는

pkg/apis 경로를 모르면 오류가 발생하기 때문.

생성된 deep copy파일은 나중에 원하는 경로로 옮기는 방식으로 함.

 

[iksooncrd:v1]

<resource_name>:<version>

예제환경의 

/root/k8s/go_test/go_crd/pkg/apis/iksooncrd

의 iksooncrd와 

해당 directory안에 있는 version 정보를 넣어줌

go_crd/pkg/client 경로에서 해당 설정을 보고

informer directory를 찾아감.

 

 

6.2. 생성 결과

[ 명령어 실행 결과 상태 ]

정상적으로 실행됨.

 

[ 생성되는 파일 ]

 

[deep copy 파일 생성 결과]

상위에서 설명했던대로

$GOPATH/src의 상태경로에 생성됨

../../../pkg/apis 로 설정했으니 /root/k8s 경로에

pkg/apis/iksooncrd/v1 directory를 생성하고 해당 경로에 

zz_generated.deepcopy.go 파일을 생성.

 

엉뚱한 경로에 생성됐으니 zz_generated.deepcopy.go 파일을

본래 생성됐어야할 위치로 이동시킴.

 

Informer, Client package 생성 결과 ]

../pkg/client 로 설정되어 

$GOPATH/src 상태경로에 정상적으로 생성됨.

아래와 같이 code generator가 생성한

clientset, informers, listers 가 있음.

 

 

 

6.3. 생성된 Informer, Client Package에 몇가지 문제가 있음.

 

아직 code generator 사용법이 미숙해서 

완벽한 Informer와 Client Package가 생성되지 않음

그에 따른 문제 해결법 아래에 설명함.

 

6.3.1. github.com이 아닌 상대경로로 code generator 사용 시 주의사항

보통 예제를 보면 

./generate-groups.sh all  github.com/test/pkg/client  github.com/test/pkg/apis  iksooncrd:v1

와 같이 경로를 github주소로 부여함 

하지만 예제에서는 

./generate-groups.sh all  ../pkg/client  ../../../pkg/apis iksooncrd:v1

이와 같이 github대신 상대경로로 설정한 상태인데

 

상대경로로 생성된 Informer, client package 의 경우

code에 설정 된 경로 또한 상대경로로 되어있어서

빌드 시 import 오류가 발생함.

이 문제를 해결하기 위해 일일이 코드에 들어가서 import 경로를 올바르게 수정하는 작업을 해야해서

많이 번거로움

아래가 상대경로를 수정한 예임.

예제에서는 상대경로로 했지만

실제 사용할 때는 github.com 주소로 설정하는것을 추천함

 

 

6.3.2. crdplural.iksoon.test.com 을 바라봐야하는데 crdkinds.iksoon.test.com 으로 설정됨

 

go_crd/pkg/client/clientset/versioned/typed/iksooncrd/v1/crdkind.go

파일을 보면 Resource("crdkinds") 로 설정되어있어서

문제가 발생함.

해당 설정을 Resource("crdplural") 로 변경해주면 정상적으로

CRD를 Watch 함

 

 

 


 

 

7. Custom Controller 생성

 

상위 과정까지 완료했으면 이제 kubernetes cluster에 CRD 자원이 생성되면

해당 이벤트를 탐지하는 라이브러리를 생성 완료한 상태임.

 

이제 생성한 Informer와 Client Package 를 사용해서 

Custom Controller를 생성해줘야함.

 

간단한 예제로 

상위에서 설정한 custom resource가 생성되면

mysql 혹은 tomcat deployment 가 생성되도록 하는 Custom Controller를 만들어봄.

오로지 Create 이벤트만 탐지하는 코드임.

 

[예제환경 : /root/k8s/go_test/go_crd/controller/controller.go]

package controller




import (
    "context"
    "fmt"
    "time"




    v1 "../pkg/apis/iksooncrd/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    "k8s.io/klog"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/tools/record"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/client-go/tools/cache"
    "k8s.io/client-go/util/workqueue"
    "k8s.io/client-go/kubernetes"


    iksoonv1 "../pkg/apis/iksooncrd/v1"
    iksoonclientset "../pkg/client/clientset/versioned"
    iksoonschemev1 "../pkg/client/clientset/versioned/scheme"
    iksooninformersv1 "../pkg/client/informers/externalversions/iksooncrd/v1"
    iksoonlisterv1 "../pkg/client/listers/iksooncrd/v1"


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


    typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    appsinformers "k8s.io/client-go/informers/apps/v1"
    appslisters "k8s.io/client-go/listers/apps/v1"
)




const controllerAgentName = "iksooncrd-controller"
 




type Controller struct {
    // kubernetes의 Core resource를 관리하기위해 필요. 
    // Kubernetes API와 통신한다.
    // iksooncrd Custom Resource가 생성되면
    // kubernetes cluster에 Deployment 를 생성하기 위해 사용됨.
    kubeclient        kubernetes.Interface


    // Custom Resource 자체를 관리하기 위해서 필요. 
    // Code generator로 만든 iksooncrd custom clientset과 통신함.
    iksoonclient      iksoonclientset.Interface
 
        //  kubernetes cluster 내에 있는 iksooncrd custom resource들을 조회하는 역할.
    iksoonLister      iksoonlisterv1.CrdKindLister
 
    // 현재 kubernetes cluster 내부에 생성된 Custom resource 상태와
    // etcd에 기록되어있는 desired state가 같은지 조회할 수 있는 함수.
    iksoonSynced      cache.InformerSynced




    // queue is a rate limited queue. This is used to queue work to be
    // processed instead of performing it as soon as a change happens.
    // This means we can ensure we only process a fixed amount of resources
    // at a time, and makes it easy to ensure we are never processing the
    // same item simultaneously in two different controllers.
    workqueue workqueue.RateLimitingInterface




    // recorder is an event recorder for recording Event resources to the
    // Kubernetes API.
    // 정의한 custom resource에서 발생하는 이벤트들을 기록하는 역할
    recorder record.EventRecorder
}




func NewController(
    kubeclient         kubernetes.Interface,
    iksoonclient       iksoonclientset.Interface,
    iksoonInformer     iksooninformersv1.CrdKindInformer) *Controller {


    utilruntime.Must(iksoonschemev1.AddToScheme(scheme.Scheme))
    klog.V(4).Info("Creating event broadcaster")
    eb := record.NewBroadcaster()
    eb.StartLogging(klog.Infof)
    eb.StartRecordingToSink(&typedcorev1.EventSinkImpl{
        Interface: kubeclient.CoreV1().Events("default"),
    })


    recorder := eb.NewRecorder(scheme.Scheme, corev1.EventSource{
        Component: controllerAgentName,
    })




    c := &Controller{
        kubeclient:        kubeclient,
        iksoonclient:      iksoonclient,
        iksoonLister:      iksoonInformer.Lister(),
        iksoonSynced:      iksoonInformer.Informer().HasSynced,
        workqueue:         workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Workers"),
        recorder:          recorder,
    }




    // Set up an event handler for when CrdKind resources change
    klog.Info("Setting up event handlers")
    iksoonInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: c.enqueueWorker,
        UpdateFunc: func(old, new interface{}) {
            c.enqueueWorker(new)
        },
    })




    return c
}








// Run will setup the event handlers for types we are interested in, as well as syncing
// informer caches and starting workers. It will block until stopCh is closed, at which point
// it will shutdown the queue and wait for workers to finish processing their current work items.
func (c *Controller) Run(workers int, stopCh <-chan struct{}) error {
    defer utilruntime.HandleCrash()
    defer c.workqueue.ShutDown()




    klog.Info("Starting Controller")




    // Wait for all caches to be synced, before processing items from the queue is started
    klog.Info("Waiting for informer caches to sync")
    if !cache.WaitForCacheSync(stopCh, c.iksoonSynced) {
        return fmt.Errorf("failed to wait for caches to sync")
    }




    klog.Info("Starting workers")
    // Launching additional goroutines would parallelize workers consuming from the queue
    for i := 0; i < workers; i++ {
        go wait.Until(c.runWorker, time.Second, stopCh)
    }




    klog.Info("Started workers")
    <-stopCh
    klog.Info("Shutting down workers")




    return nil
}




// runWorker is a long-running function that will continually call the
// processNext function in order to read and process a message on the queue.
func (c *Controller) runWorker() {
    for c.processNextWorkItem() {
    }
}










// processNext will read a single work item off the queue and
// attempt to process it, by calling the syncHandler
func (c *Controller) processNextWorkItem() bool {
    // Wait until there is a new item in the working queue
    item, quit := c.workqueue.Get()
    if quit {
        return false
    }
 


    err := func(item interface{}) error {


        defer c.workqueue.Done(item)
        var key string
        var ok bool


        if key, ok = item.(string); !ok {
            // As the item in the queue is actually invalid, we call Forget
            // here else we'd go into a loop of attempting to process a work item that is invalid.
            c.workqueue.Forget(item)
            utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", item))
            return nil
        }




        // Run the syncHandler, passing it the namespace/name string of the
        // crdkind resource to be synced.
        if err := c.syncHandler(key); err != nil {
            // Put the item back on the queue to handle any transient errors.
            c.workqueue.AddRateLimited(key)
            return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
        }




        // Finally, if no error occurs we Forget this item so it does not
        // get queued again until another change happens.
        c.workqueue.Forget(item)
        klog.Infof("Successfully synced '%s'", key)
        return nil
    }(item)




    if err != nil {
        utilruntime.HandleError(err)
        return true
    }




    return true
}






// syncHandler compares the actual state with the desired, and attempts to
// converge the two. It then updates the Status block of the CrdKinds resource
// with the current status of the resource.
func (c *Controller) syncHandler(key string) error {
    // Convert the namespace/name string into a distinct namespace and name
    namespace, name, err := cache.SplitMetaNamespaceKey(key)
    if err != nil {
        utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
        return nil
    }




    // Get the CrdKinds resource with this namespace/name
    crdiksoon, err := c.iksoonLister.CrdKinds(namespace).Get(name)
    if err != nil {
        // The crdiksoon resource may no longer exist, in which case we stop
        // processing.
        if errors.IsNotFound(err) {
            utilruntime.HandleError(fmt.Errorf("crdiksoon '%s' in work queue no longer exists", key))
            return nil
        }




        return err
    }




    choice := crdiksoon.Spec.Choice
    if choice == "" {
        utilruntime.HandleError(fmt.Errorf("%s: crdiksoon choice must be specified", key))
        return nil
    }


    replicas := crdiksoon.Spec.Replicas
    if replicas == nil {
        utilruntime.HandleError(fmt.Errorf("%s: crdiksoon replicas must be specified", key))
        return nil
    }
 
    c.kubeclient.AppsV1().Deployments(crdiksoon.Namespace).Create(context.TODO(), createIksooncrdDeployment(crdiksoon), metav1.CreateOptions{})


    c.recorder.Event(crdiksoon, corev1.EventTypeNormal, "Synced", "ikcooncrd synced successfully")
    return nil
}




func createIksooncrdDeployment(iksooncrd *iksoonv1.CrdKind) *appsv1.Deployment {


    var imageName string
    var replicas *int32
 
    if "mysql" == iksooncrd.Spec.Choice {
        imageName = "peksoon/iksoon_mysql:1.0.2"
    } else if "tomcat" == iksooncrd.Spec.Choice {
        imageName = "peksoon/iksoon_tomcat:1.0.6"
    }   


    replicas = iksooncrd.Spec.Replicas
 
 
    labels := map[string]string{
        "crd":        "deployment-test",
    }
    return &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "crddeployment",
            Namespace: "default",
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: labels,
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: labels,
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  iksooncrd.Spec.Choice,
                            Image: imageName,
                        },
                    },
                },
            },
        },
    }
}








// enqueueWorker takes a crdkind resource and converts it into a namespace/name
// string which is then put onto the work queue. This method should not be
// passed resources of any type other than crdkind.
func (c *Controller) enqueueWorker(obj interface{}) {
    var key string
    var err error
    if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
        utilruntime.HandleError(err)
        return
    }
    c.workqueue.Add(key)
}



 

 

 

Go로 Controller를 생성해본 결과

Python과 비교를 해보자면

Python의 경우에는 kopf를 사용하면 간단하게 create, delete 등 이벤트를 탐지할 수 있었는데 

Go를 사용해서 Controller를 생성해보면 이러한 예제와 같이

개발자가 직접 Create, Delete 등등 이벤트를 설정해줘야함

 

python보다 어렵지만 

Go는 섬세한 코드 설정이 가능함.

 

 

[참고] 

https://github.com/kubernetes/sample-controller/blob/master/controller.go

https://github.com/zeroFruit/kubernetes-controller-demo/blob/master/controller/controller.go

 

 

 


 

 

 

7. Main문 작성

 

[main.go]

package main




import (
    "os"
    "time"
    controller "../controller"


    "k8s.io/client-go/tools/clientcmd"
    iksoonclientset "../pkg/client/clientset/versioned"
    iksooninformersv1 "../pkg/client/informers/externalversions"
    kubeinformers "k8s.io/client-go/informers"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/klog"
)




func main() {
    // klog print to console
    klog.SetOutput(os.Stdout)




    // set up signals so we handle the first shutdown gracefully
    stopCh := make(chan struct{})
    defer close(stopCh)




    config := getClientConfig()
    kubeClient, err := kubernetes.NewForConfig(config)
    if err != nil {
        klog.Fatalf("Failed to create kubernetes client: %v", err)
    }




    iksoonClient, err := iksoonclientset.NewForConfig(config)
    if err != nil {
        klog.Fatalf("Failed to create Test CRD client: %v", err)
    }




    kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*1000)
    iksoonInformerFactory :=iksooninformersv1.NewSharedInformerFactory(iksoonClient, time.Second*1000)




    c := controller.NewController(
        kubeClient,
        iksoonClient,
        kubeInformerFactory.Apps().V1().Deployments(),
        iksoonInformerFactory.Iksoon().V1().CrdKinds(),
    )




    // notice that there is no need to run Start methods in separate goroutine.
    // Start method is non-blocking and runs all registered informers in a dedicated goroutine/
    kubeInformerFactory.Start(stopCh)
    iksoonInformerFactory.Start(stopCh)




    if err := c.Run(2, stopCh); err != nil {
        klog.Fatalf("Error running controller: %v", err)
    }
}




// getClientConfig retrieve the Kubernetes client from outside of the cluster
func getClientConfig() *rest.Config {
    // construct the path to resolve to `~/.kube/config`
    kubeConfigPath := os.Getenv("KUBECONFIG")


    config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
    if err != nil {
        klog.Fatalf("Error get Kubernetes config: %v", err)
    }
    return config
}

 

 

 

 


 

 

8. 결과 확인

 

8.1. Custom Controller 실행

상위에서 Custom Resource Definition는 생성한 상태에서 진행.

 

[Go실행하는 Server 환경]

해당 Server에 kubectl 이 설치되어있고

KUBECONFIG 환경변수가 설정되어있는 상태.

즉 kubectl 명령으로 kubernetes cluster 에 정상적으로 명령을 보낼 수 있는 상태의 서버임.

 

 

[명령어]

go get ./cmd/

go run ./cmd/

정상적으로 Custom Controller 가 실행됨.

 

8.2. 결과 확인

 

Master Node 에서  CRD에 해당하는 object를 생성함.

[예제  yaml파일]

apiVersion: "iksoon.test.com/v1"
kind: CrdKind
metadata:
  name: iksoon-crd-test
spec:
  choice: mysql
  replicas: 1

 

 

 

생성을 하면 Custom Controller 에서 create 이벤트 탐지 로그 확인가능.

 

 

결과를 확인해보면 정상적으로 mysql deployment 가 생성된것을 확인할 수 있음

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

오류 정리

 

invalid version: git fetch --unshallow -f origin in

 

[사용한 명령어]

./generate-groups.sh all github.com/peksoon/crdtest/go_crd/pkg/client github.com/peksoon/crdtest/go_crd/pkg/apis "iksoontest:v1"

 

[error msg]

go: k8s.io/kube-openapi@v0.0.0-20200923155610-8b5066479488 requires

        github.com/go-openapi/jsonpointer@v0.0.0-20160704185906-46af16f9f7b1: invalid version: git fetch --unshallow -f origin in /root/k8s/go_test/go_crd/pkg/mod/cache/vcs/a3358edaa0410f6589a68105419a2e9a909bedc0965d11239b88793a8d20bbf7: exit status 128:

        fatal: git fetch-pack: expected shallow list

 

[해결법]

sudo yum remove git*

sudo yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.7-1.x86_64.rpm

sudo yum -y install git

 

참고 :  https://computingforgeeks.com/how-to-install-latest-version-of-git-git-2-x-on-centos-7/

 

 

 

 

 

Error: Failed making a parser: unable to add directory

 

[사용한 명령어]

./generate-groups.sh all /root/k8s/go_test/go_crd/pkg/client ../../../pkg/apis "iksoontest:v1"

 

[error msg]

Generating deepcopy funcs

F1013 13:22:45.282844    4802 main.go:82] Error: Failed making a parser: unable to add directory "../../../pkg/apis/iksoontest/v1": unable to import "../../../pkg/apis/iksoontest/v1": cannot find package "../../../pkg/apis/iksoontest/v1" in:

        /root/k8s/go_test/go_crd/pkg/apis/iksoontest/v1

[해결법]

./generate-groups.sh 명령 시 client 경로와 pkg/apis 경로를 정확히 설정해야함

 

참고 : https://github.com/kubernetes/code-generator/issues/55

 

 

 

 

 

 


제 글을 복사할 시 출처를 명시해주세요.
글에 오타, 오류가 있다면 댓글로 알려주세요! 바로 수정하겠습니다!


 

 

 

 

 

[code generator 참고]

https://github.com/kubernetes/code-generator

https://www.openshift.com/blog/kubernetes-deep-dive-code-generation-customresources

https://insujang.github.io/2020-02-13/programming-kubernetes-crd/

https://itnext.io/how-to-generate-client-codes-for-kubernetes-custom-resource-definitions-crd-b4b9907769ba

https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html

 

 

 

 

 

 

 

 

 

 

 

 

반응형