0. kubebuilder 사용하여 커스텀 컨트롤러 생성해보기

동우김·2023년 10월 30일
0

쿠버네티스

목록 보기
1/1
post-thumbnail
  • 참고 : 이 글은 kubebuilder 공식 문서의 튜토리얼을 따라 custom controller를 만들어보는 과정을 작성한 문서입니다.

개요


Kubebuilder는 custom resource definition(CRD)를 사용해 Kbuernetes API를 만드는데 사용되는 프레임워크입니다.

이를 사용하여 custom resource에 사용되는 controller를 쉽게 만들 수 있습니다. 또한 컨트롤러를 위한 기본적인 디렉터리 구조와 소스코드, Makefile 등을 생성해 주는 툴을 제공합니다.

이 포스트에서는 Kubebuilder의 기본적인 설치 방법과 사용 방법. 이를 사용한 간단한 Controller 작성에 대해 다룬다.

KubeBuilder란?


Kubernetes-SIG (Special Interst Group)에서 제공하는 프로젝트 중 하나로, controller를 만들기 위해서는 보통 3가지 옵션이 있다고 합니다.

  • Kubebuilder
  • Operator SDK
  • kubernetes-sigs/controller-runtime

다만 내부적으로는 Kubebuilder와 Operator SDK 모두 controller runtime을 사용중이기도하여, 굳이 controller runtime으로 처음부터 무언가를 만들 필요는 없지 싶습니다.

Operator SDK의 FAQ 페이지에선 Kubebuilder와의 차이점에 대해서 이렇게 설명하고 있습니다.

  • 기본적으로 Operator SDK와 Kubebuilder는 큰 차이가 없음
    • 둘 다 operator 프로젝트 만드는데 사용되는 편의성 툴
    • 모두 내부적으로는 controller runtime을 사용
    • 사실, 내부적으로 Operator SDK는 kubebuilder를 사용중이기도 함
    • 때문에 프로젝트의 레이아웃은 같고, 심지어 operator sdk의 cli는 kubebuilder cli를 완벽 지원함
  • 그럼 어떤 차이가 있는가?
    • Operator Lifecycle Manager 제공 (업데이트 관려 도구라고 생각하자)
    • 커뮤니티인 Operator Hub와의 통합
    • operator 평가용 툴인 scorecard 지원

Operator Hub에는 Opentelemetry Oprator나, Datadog Operator, AWS 관련 컨트롤러 등이 있는 것을 확인할 수 있습니다. 때문에 자사 서비스를 외부에 SaaS 형태로 제공하고, 이를 Kubernetes 환경에서 지원해야 하는 상황이라면 궁극적으로는 Operator SDK를 사용하게 되지 않을까? 하는 생각이 드네요

그러나 이번에 만들 샘플 앱이 그렇게 될 일은 없으므로, kube builder를 사용하여 진행하도록 하겠습니다.

아키텍처

들어가기에 앞서, kubebuilder가 어떤 아키텍처로 이루어져 있는지 알아봅시다.

공식 document 문서에는 다음과 같이 나와있습니다.

kubebuilder architecture

아키텍처 상에는 몇가지 정보가 나와있는데, 정리하자면 다음과 같습니다. (a/b 라고 쓸때, b 모듈이 a 모듈 하위에 있음을 의미합니다)

공통

  • 기본적으로 Controller는 HA가 가능하다. 다만 이때 leader election 로직이 사용된다.
  • kubebuilder는 내부적으로 kubernetes-sig의 controller-runtime 패키지를 사용한다.

프로세스/매니저

  • 매니저는 아래의 몇가지 기본 기능을 담당한다.
    • Leader Election
    • Metric 노출
    • webhook cert 핸들링
    • 이벤트 캐싱 (kubernetes api 서버의 부하를 줄이기 위해)
    • Clients 관리
    • 다른 Controller들로의 이벤트 브로드캐스팅
    • signal 과 shutdown 핸들링

프로세스/매니저/컨트롤러

  • reconcile 되는 kind 하나당 하나가 존재
  • 이것으로 부터 생성된 리소스를 소유함 (이 페이지를 참고하자)
  • cach와 client를 사용하며, filter(Predicate)를 거쳐 이벤트를 수신함
  • 이벤트를 받을 때 마다 reconciler를 동작시킴
  • 이벤트에 대해 back-off 와 queueing, re-queueing을 수행함
    • 그러니까, kubernetes event를 내부의 queue에서 빼오고
    • 실패한 이벤트에 대해 다시 queue에 집어넣고
    • 이에 대한 재시도 간격을 rate limiter를 사용하여 exponential하게 증가시킨다

프로세스/매니저/웹훅

  • 0개 또는 1개의 웹훅에 대해, reconcile되는 kind 별로 하나가 생성됨
  • Admission Request에 대해 Defaulter와 Validator를 실행시킨다

설치 방법


이어서 기본적인 설치와 사용법을 익혀보겠습니다.

Prerequisite

23년 10월 21일 기준, 사전 조건은 다음과 같습니다.

  • go version v1.20.0+
  • docker verison 17.03+
  • kubectl version v1.11.3+
  • 접속 가능한 Kubernetes v1.13.3+ cluster

최신 조건은 공식 document에서 확인 가능합니다.

Installation

다음 명령어로 설치할 수 있습니다.

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

kubebuilder로 cronjob controller 만들기

쿠버네티스에는 이미 cronjob controller가 존재합니다. 다만, 이번 테스트에서는 kubebuilder 공식 documnet에서 제공하는 tutorial을 따라가며 custom resource로 제어되는 cornjob 컨트롤러를 만들어 볼 예정입니다.

1. 프로젝트 만들기

새로운 리소스를 만들기 위해서 domain을 설정하여야 합니다. domain을 my.corn이라고 한다고 했을때, 다음 명렁어로 프로젝트를 만들 수 있습니다.

mkdir dummycron
cd dummycron
kubebuilder init --domain my.cron --repo my.cron/dummycron

이 결과로, 다음과 같은 프로젝트가 생성되었습니다.

.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── cmd
│   └── main.go
├── config
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.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
│       ├── role_binding.yaml
│       └── service_account.yaml
├── go.mod
├── go.sum
└── hack
    └── boilerplate.go.txt

프로젝트 구조 들여다보기

자 그럼, 각 프로젝트는 어떤 것을 의미할까요?

  • Makefile : controller를 배포하고 빌드하는데 필요한 target들이 정의된다
  • PROJECT : 컴포넌트들에 대한 Kubebuilder meatadata가 정의된다
  • config/ : 서버 배포에 필요한 yaml 파일과, Kustomize 파일이 포함되어있다
  • config/default : 표준 설정으로 controller를 배포하기 위한 kustomize base 파일 포함
  • config/manager : controller를 pod 형태로 배포하기 위한 설정
  • config/rbac : 컨트롤러의 service account에 바인드 되어야 하는 권한 설정
  • cmd/main.go : 컨트롤러가 실행될 때의 기본 main 함수가 들어있음

기본적으로 main 함수에서 Metric 관련된 몇가지 flag를 받고, Manager를 초기화/실행하는 것을 확인할 수 있었습니다.

코드 살펴보기

그럼 이제 생성된 코드를 훑어보겠습니다.

  1. cmd/main.go

    API에 필요한 Scheme을 생성하고, cache를 초기화하며 Manager를 생성합니다.

,,,
func main() {
	// 생략
	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
    // ... 플래그를 초기화 합니다 ...
    opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme, // main에서 생성한 scheme을 받습니다
        // 생략
	})
   // ... 생략 ...
   	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}
}

Manager 초기화와 동시에 Cache 또한 초기화 되므로, 다음과 같이 cache에 대한 옵션을 조절할 수도 있습니다. 예를 들어, default namespace의 cache만 받고 싶다면, 다음과 같이 코드를 수정할 수 있을 것입니다.

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		Cache:                  cache.Options{DefaultNamespaces: map[string]cache.Config{"default": {}}},
    })

2. API 만들기

TL;TR

다음 명령어를 입력하고, 나오는 모든 질문에 y를 입력합니다.

kubebuilder create api --group batch --version v1 --kind CronJob

용어 정리

API를 만들기 전에, 사용되는 용어들을 짚고 넘어가겠습니다.

  • Kind: Kind는 객체의 타입이다. Kubernetes의 object에는 Kind 필드가 존재한다. 이는 client 등에 이 리소스가 어떤 것인지 알려주는 역할을 한다. 또한 golang에서의 쿠버네티스 개발에서, 하나의 Kind는 하나의 struct에 대입된다. Kind는 다시 크게 3가지 타입으로 나눌 수 있다.
    • 시스탬 내에 존재하는 객체 : 예를 들어, pod가 있을 수 있다.
    • List : List는 하나 이상의 객체가 포함된 컬렉션을 의미한다. 예를 들어, Pod에 대해 List API를 호출할때, 이 Kind의 객체가 반환된다.
    • 특수 목적 Kind: 예를 들어, /scale 이나 /binding 등이 존재한다. 이런 Kind는 어떤 동작을 나타낸다. deployment/scale이나 replicaset/scale 등을 생각해볼 수 있다.
  • API Group: 각 Kind들은 논리적으로 엮여있는 경우가 많다. (job과 cronjob을 생각해보자) 이런 Kinde들은 하나의 API group으로 엮이게 된다.
  • Version: 각 API Group은 여러가지 버전으로 존재할수 있다. (batch/v1과 batch/v1beta를 생각해보자) Kubernetes 업데이트에 따라, 새로운 API가 포함될때 일반적으로 v1alpha1 으로 정의된다. 이것이 안정됨에 따라, v1beta1을 지나 v1으로 버전이 변하곤 한다.
  • Resource: /pod 등과 같은 것인데, CRUD 대상이 되는 HTTP 엔드포인트를 나타낸다고 생각하면 된다. 일반적으로 이 엔드포인트들은 CRUD를 충족하며, 하위 path를 추가하여 기타 동작을 수행할 수도 있다. (예를 들어, /pod/nginx/log 는 nginx라는 이름의 pod의 log를 반환할 것이다) 각 엔드포인트는 Kind를 반환한다. 단, 에러 발생시 Status가 반환된다

각 리소스는 Group Version Resource(GVR)로 구분됩니다. 일반적으로, kubernetes endpoint에 다음과 같은 경로로 질의를 보낼 수 있을 것입니다.

/apis/$GROUP/$VERSION/namespaces/$NAMESPACE/$RESOURCE

단, Node나 PV 같이 namespaced 리소스가 아닌 것들은 별도로 namespace로 조회하지 않는다. 이런 경우, Group Version Kind(GVK)로 구분 할 수 있습니다.

실제 동작하는지 확인을 위해 다음과 같은 테스트를 해 보았습니다.

# HTTP 인증 등을 회피하기 위해 proxy를 연다 
$ kubectl proxy &
Starting to serve on 127.0.0.1:8001

# 테스트 데이터 세팅
$ kubectl create deployment nginx --image=nginx --replicas=2
deployment.apps/nginx created

# deployment 목록 조회
$ curl localhost:8001/apis/apps/v1/namespaces/default/deployments
{
  "kind": "DeploymentList",
  "apiVersion": "apps/v1",
  "metadata": {
    "resourceVersion": "10167"
  },
  "items": [
    {
      "metadata": {
        "name": "nginx",
        "namespace": "default",
        "uid": "3a700d47-2dc1-453f-8d2c-d1a96472b590",
        "resourceVersion": "9834",
... 생략 ...

# 특정 이름의 deployment 조회
$ curl localhost:8001/apis/apps/v1/namespaces/default/deployments/nginx   
{
  "kind": "Deployment",
  "apiVersion": "apps/v1",
  "metadata": {
    "name": "nginx",
    "namespace": "default",
    "uid": "3a700d47-2dc1-453f-8d2c-d1a96472b590",
... 생략 ...

 # 이번에는 node를 조회해보자. core 리소스는 apis가 아니라 api로 조회된다.
 $ curl localhost:8001/api/v1/nodes                  
{
  "kind": "NodeList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "10522"
  },
  "items": [
    {
      "metadata": {
        "name": "minikube",
        "uid": "e62e659c-6bd7-49fd-b931-ffbe2090fd70",
... 생략 ...

API 추가하기

이제 kubebuilder를 사용하여 api를 추가합니다. 터미널에 다음과 같이 입력한 후, 나오는 모든 질문에 y를 입력합니다.

kubebuilder create api --group batch --version v1 --kind CronJob

디렉터리에 api/v1과 bin 등이 추가된 것을 확인 할 수 있습니다.

.
├── Dockerfile
... 생략 ...
├── api
│   └── v1
│       ├── cronjob_types.go
│       ├── groupversion_info.go
│       └── zz_generated.deepcopy.go
├── bin
│   └── controller-gen
... 생략 ...
└── internal
    └── controller
        ├── cronjob_controller.go
        └── suite_test.go

어떤 내용이 생성되었는지 잠깐 살펴보면, 다음과 같습니다

  1. cronjob_types.go :

이 파일에는 Controller에서 사용되는 object들이 정의되어 있으며, 개발자는 이 부분을 채워넣어야 합니다.

다음과 같은 주석을 확인할 수 있습니다.

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

별 다른 작업을 하지 않더라도, 기본적으로 다음과 같은 구조체들이 선언되어 있음을 확인 할 수 있습니다.

여기엔 생략된 CronjobSpec과 CronjobStatus까지 합해 모두 4개의 구조체가 정의되어 있습니다.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // Kubernetes Kind들에 대한 메타데이터를 다루는 패키지
)

// ... 생략 ...

// CronJob is the Schema for the cronjobs API
type CronJob struct {                             // kubebuilder에 명세한 Kind에 대응되는 struct
	metav1.TypeMeta   `json:",inline"`            // API 버전과 Kind에 대한 구조체
	metav1.ObjectMeta `json:"metadata,omitempty"` // name, namespace, label 등의 정보를 담는 구조체

	Spec   CronJobSpec   `json:"spec,omitempty"`  // 각 객체에 대한 명세가 담길 구조체
	Status CronJobStatus `json:"status,omitempty"`// 각 객체에 대한 상태가 담길 구조체
}

//+kubebuilder:object:root=true

// CronJobList contains a list of CronJob
type CronJobList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []CronJob `json:"items"`
}

func init() {
	SchemeBuilder.Register(&CronJob{}, &CronJobList{}) // 여기서 정의되는 type들을 API group에 추가하는 부분입니다
}

Kubernetes API 생성 규칙

몇가지 특징이 있습니다.

  1. 모든 필드는 camelCase로 작성된다. 또한 jsonTag 마찬가지로 camelCase로 작성된다
  2. optional한 필드에는 omitempty가 붙는다
  3. 대다수의 필드가 단순 원시필드로 정의됩니다. 예외 케이스 몇가지가 있는데, 다음과 같습니다
  • 숫자는 하위 호환성을 위해 int32와 int64 모두를 지원한다
  • 소수의 경우, resource.Quantity를 사용합니다(그러니까, resource.limit.cpu에 1과 0.5 모두를 사용할 수 있음을 생각해봅시다)
  • 시간을 나타내기 위해서는 metaV1.Time을 사용합니다. time.Time의 단순한 래퍼인데, JSON과 YAML용 마샬러를 지원합니다.

이상 kubebuilder로 생성된 api 코드를 짧게 보았습니다. 다음 포스트에서는 커스텀 cronjob을 채워나가는 튜토리얼을 이어서 하도록 하겠습니다.

profile
빈둥거리는 것에 관심이 많습니다

0개의 댓글