golang과 operator-sdk을 통해서 개발을 진행해보도록 하자.
먼저 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를 소개하자면 다음과 같다.
operator에 대한 template를 만들었지만, 내용은 완전 텅 비어있기 때문에 비어있는 controller가 실행된다. 이는 Readyz
와 Healthz
endpoint들만 가지고 실행되고 있는 것이다.
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를 통해서 다음이 만들어지게 된 것이다.
api/
라고 불리는 directory에 생성operator.example.com
으로 되어있다. 이는 우리가 operator를 실행할 때 example.com
domain으로 잡았기 때문이다.v1alpha1
이다.NginxOperator
이다.controllers/
directory아래에 boilerplate controller code가 있다. 앞으로 여기에서 작업이 진행될 것이다.main.go
에서 새로운 controller를 실행시키는 boilerplate code가 추가된다. api/v1alpha1/
directory를 보면 nginxoperator_types.go
파일 이외에 두 개의 file로 groupversion_info.go
와 zz_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들은 모두 Spec
과 Status
field를 가진다. 이는 operator 역시도 마찬가지이다. 이 두가지 feild를 활용하여 user input을 받아들이고, operator의 status를 보고하게 된다. 따라서, 다음과 같은 구조를 갖게 되는 것이다.
---------------
|NginxOperator|
---------------
|
--------------------------------
| |
------------------- ---------------------
|NginxOperatorSpec| |NginxOperatorStatus|
------------------- ---------------------
| |
--------------------------------
|
--------------
|Control Loop|
--------------
Spec
부분에 operator CR 구동에 필요한 option들을 추가하면 된다. 우리의 경우는 operator의 operand로 nginx를 구동시킬 것이기 때문에 다음의 option들을 넣도록 하자.
위의 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
로 변환되고 Replicas
는 replicas
로 변환되는 것이다. 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"`
이제 우리의 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들이 들어있는데, 안쓸 것들은 빼는 것이 좋다. 그러나, 아마 대부분 귀찮아서 그냥 이대로 쓴다.
나머지 남은 resource들은 ClusterRole
과 nginx pod를 구동시킬 Deployment
이다. ClusterRole
은 kubebuild
tags를 통해서 code로 생성할 수 있다. 이에 대해서는 control loop를 만드는 section에서 다루기로 하자. 먼저 Deployment
를 정의하여 control loop에서 접근할 수 있도록 하자.
이전에도 말했지만, kubernetes client library를 사용하여 deployment와 같은 kubernetes object를 만드는 방법은 유지 보수 방법에서 그렇게 좋지가 않다. yaml 파일 그대로를 들고 있는 것이 좋다. 이를 위해서 yaml
파일을 가지고 있을 assets
directory를 만들고, manifests directory를 만든 다음 아래의 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을 받을 변수를 정해주면 된다.
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들을 변경하고 수정할 수 있기 때문이다.
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.Result
와 error
를 받는데, 위의 코드에서는 빈 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를 만들어보도록 하자.
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
}
errors
는 k8s.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에 대한 name
과 namespace
를 담고 있는 구조체로 보면 된다. 단, operator와 같은 namespace로 배포되는 것을 잊지말도록 하자.
operator CR을 얻을 수 있다는 것은 operator spec
부분에 접근이 가능하다는 것이다. 이제 spec
을 기반으로 operand에 설정을 해주어야하는데, 우리의 경우 nginx deployment를 만드려고 하는 것이다. 이를 위해서 deployment
가 먼저 있는 지 확인해보도록 하자. 만약, 없다면 위에서 설정한 nginx_deployment.yaml
파일을 가져와서 spec부분에 맞게 data를 설정해주기만 하면 된다.
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는 안되는 것이다. 어차피 create
든 update
든 로직은 같으므로 다음과 같이 공통화된 부분을 묶어 code를 만들 수 있다.
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
}
create
와 update
에 대한 공용화 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의 ClusterRole
이 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()
로 설정할 수 있다.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&operatorv1alpha1.NginxOperator{}).
Complete(r)
}
해당 code는 cluster에서 NginxOperator
에 관한 변화를 관찰하기 위해서 생성되었다. 그러나 우리는 Deployment object에 대한 변화 역시도 관찰해야한다. 따라서, 함수를 다음과 같이 변경하기로 하자.
// 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 구현이 완료된 것이다.
배포하기 전에 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를 넣어주도록 하자.
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