Kubebuilder는 custom resource definition(CRD)를 사용해 Kbuernetes API를 만드는데 사용되는 프레임워크입니다.
이를 사용하여 custom resource에 사용되는 controller를 쉽게 만들 수 있습니다. 또한 컨트롤러를 위한 기본적인 디렉터리 구조와 소스코드, Makefile 등을 생성해 주는 툴을 제공합니다.
이 포스트에서는 Kubebuilder의 기본적인 설치 방법과 사용 방법. 이를 사용한 간단한 Controller 작성에 대해 다룬다.
Kubernetes-SIG (Special Interst Group)에서 제공하는 프로젝트 중 하나로, controller를 만들기 위해서는 보통 3가지 옵션이 있다고 합니다.
다만 내부적으로는 Kubebuilder와 Operator SDK 모두 controller runtime을 사용중이기도하여, 굳이 controller runtime으로 처음부터 무언가를 만들 필요는 없지 싶습니다.
Operator SDK의 FAQ 페이지에선 Kubebuilder와의 차이점에 대해서 이렇게 설명하고 있습니다.
kubebuilder
cli를 완벽 지원함Operator Hub에는 Opentelemetry Oprator나, Datadog Operator, AWS 관련 컨트롤러 등이 있는 것을 확인할 수 있습니다. 때문에 자사 서비스를 외부에 SaaS 형태로 제공하고, 이를 Kubernetes 환경에서 지원해야 하는 상황이라면 궁극적으로는 Operator SDK를 사용하게 되지 않을까? 하는 생각이 드네요
그러나 이번에 만들 샘플 앱이 그렇게 될 일은 없으므로, kube builder를 사용하여 진행하도록 하겠습니다.
들어가기에 앞서, kubebuilder가 어떤 아키텍처로 이루어져 있는지 알아봅시다.
공식 document 문서에는 다음과 같이 나와있습니다.
아키텍처 상에는 몇가지 정보가 나와있는데, 정리하자면 다음과 같습니다. (a/b 라고 쓸때, b 모듈이 a 모듈 하위에 있음을 의미합니다)
이어서 기본적인 설치와 사용법을 익혀보겠습니다.
23년 10월 21일 기준, 사전 조건은 다음과 같습니다.
최신 조건은 공식 document에서 확인 가능합니다.
다음 명령어로 설치할 수 있습니다.
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
쿠버네티스에는 이미 cronjob controller가 존재합니다. 다만, 이번 테스트에서는 kubebuilder 공식 documnet에서 제공하는 tutorial을 따라가며 custom resource로 제어되는 cornjob 컨트롤러를 만들어 볼 예정입니다.
새로운 리소스를 만들기 위해서 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를 초기화/실행하는 것을 확인할 수 있었습니다.
그럼 이제 생성된 코드를 훑어보겠습니다.
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": {}}},
})
다음 명령어를 입력하고, 나오는 모든 질문에 y를 입력합니다.
kubebuilder create api --group batch --version v1 --kind CronJob
API를 만들기 전에, 사용되는 용어들을 짚고 넘어가겠습니다.
Kind
: Kind는 객체의 타입이다. Kubernetes의 object에는 Kind 필드가 존재한다. 이는 client 등에 이 리소스가 어떤 것인지 알려주는 역할을 한다. 또한 golang에서의 쿠버네티스 개발에서, 하나의 Kind는 하나의 struct에 대입된다. Kind는 다시 크게 3가지 타입으로 나눌 수 있다.List
: List는 하나 이상의 객체가 포함된 컬렉션을 의미한다. 예를 들어, Pod에 대해 List API를 호출할때, 이 Kind의 객체가 반환된다.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",
... 생략 ...
이제 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
어떤 내용이 생성되었는지 잠깐 살펴보면, 다음과 같습니다
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에 추가하는 부분입니다
}
몇가지 특징이 있습니다.
camelCase
로 작성된다. 또한 jsonTag 마찬가지로 camelCase로 작성된다resource.Quantity
를 사용합니다(그러니까, resource.limit.cpu에 1과 0.5 모두를 사용할 수 있음을 생각해봅시다)metaV1.Time
을 사용합니다. time.Time의 단순한 래퍼인데, JSON과 YAML용 마샬러를 지원합니다.이상 kubebuilder로 생성된 api 코드를 짧게 보았습니다. 다음 포스트에서는 커스텀 cronjob을 채워나가는 튜토리얼을 이어서 하도록 하겠습니다.