쿠버네티스 이벤트를 이용해서 오퍼레이터의 오류를 출력하기

흔히 오퍼레이터(Operator)라고 많이 부르는, 컨트롤러(Controller)에서 오류를 효과적으로 출력하는 방법에 대해서 알아보겠습니다. 이 글에서는 오퍼레이터(Operator)라는 용어로 통일해서 사용하겠습니다.

쿠버네티스에서 CR(Custom Resource)을 생성하면, 해당 CR을 담당하는 오퍼레이터가 , 원하는 목적을 이루기 위해서 여러가지 작업을 실행합니다. CR을 생성한 사용자의 실수나, 시스템의 오류 등의 여러 이유 때문에 이러한 작업이 실패할 수 있습니다. 작업이 실패할 경우 사용자에게 오류의 이유를 알려줘야지, 사용자가 무슨 문제인지 인식 할 수 있을 것입니다.

오류를 알려줄 수 있는 가장 쉬운 방법은 로그를 남기는 것입니다. 간단히 생각하면, 오퍼레이터의 로그에 오류 내용을 출력하면 되는것입니다. 하지만, 이 방법은 한 가지 문제를 가지고 있습니다. 바로 권한 문제입니다. CR을 생성한 사용자가, 오퍼레이터의 로그를 볼 권한을 가지고 있다고는 볼 수가 없기 때문입니다. 일반적으로 CR을 생성하는 권한과, 오퍼레이터를 관리하는 권한은 별도로 분리가 되어 있기 때문에, CR 사용자는 오퍼레이터의 로그를 볼 수가 없는 것입니다.

로그를 볼 수 없는 CR 사용자는, 어떤 이유 때문에 문제가 발생했는지를 알 수 없게 되어서, 작업을 진행하는데 심각한 어려움을 겪게 될 것입니다. 물론 오퍼레이터 관리자가 문제를 찾아서, CR 사용자 한테 전달해 줄 수는 있지만, 이 과정은 상당한 병목 현상을 발생시킬 것입니다. CR 사용자에게 어떤 부분에서 오류가 발생했는지를, 다른 사람의 개입없이 알아낼 수 있게, 정보를 제공해 주는 것이 최선의 방법일것입니다.

Kubernetes Events 는 이러한 오류에 대한 정보를 CR 사용자에게 전달하는데 효과적입니다. 포드(pod)의 이벤트 스트림에 오류 정보를 출력하게 되면, 사용자는 kubectl describe 명령어를 통해서 오류를 조회해 볼 수 있습니다.

다음은 kubectl describe pod을 실행해본 결과입니다. 해당 포드의 이벤트가 출력되는 것을 볼 수 있습니다.

$ kubectl describe pod jupyter-elsa
Name:           jupyter-elsa
Namespace:      snow
...
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                      Message
  ----    ------     ----  ----                      -------
  Normal  Scheduled  16s   default-scheduler         Successfully assigned snow/jupyter-elsa to aries
  Normal  Pulled     11s   kubelet, aries            Container image "docker.io/istio/proxy_init:1.2.9" already present on machine
  Normal  Created    11s   kubelet, aries            Created container istio-init
  Normal  Started    11s   kubelet, aries            Started container istio-init
  Normal  Pulling    9s    kubelet, aries            Pulling image "tensorflow/tensorflow:2.1.0-py3-jupyter"
  Normal  Pulled     9s    kubelet, aries            Successfully pulled image "tensorflow/tensorflow:2.1.0-py3-jupyter"
  Normal  Created    9s    kubelet, aries            Created container jupyter
  Normal  Started    8s    kubelet, aries            Started container jupyter
  Normal  Pulled     8s    kubelet, aries            Container image "docker.io/istio/proxyv2:1.2.9" already present on machine
  Normal  Created    8s    kubelet, aries            Created container istio-proxy
  Normal  Started    8s    kubelet, aries            Started container istio-proxy

이벤트 기록하기

이벤트를 기록하기 위해서는, EventRecorder 를 생성해야합니다. EventRecordercontroller-runtimemanager를 이용해서 쉽게 생성할 수 있습니다.

https://github.com/kubernetes-sigs/controller-runtime/blob/v0.4.0/pkg/manager/manager.go#L86

// Manager initializes shared dependencies such as Caches and Clients, and provides them to Runnables.
// A Manager is required to create Controllers.
type Manager interface {
...
	// GetEventRecorderFor returns a new EventRecorder for the provided name
	GetEventRecorderFor(name string) record.EventRecorder
...

다음은 EventRecorder의 코드입니다.

// EventRecorder knows how to record events on behalf of an EventSource.
type EventRecorder interface {
	// Event constructs an event from the given information and puts it in the queue for sending.
	// 'object' is the object this event is about. Event will make a reference-- or you may also
	// pass a reference to the object directly.
	// 'type' of this event, and can be one of Normal, Warning. New types could be added in future
	// 'reason' is the reason this event is generated. 'reason' should be short and unique; it
	// should be in UpperCamelCase format (starting with a capital letter). "reason" will be used
	// to automate handling of events, so imagine people writing switch statements to handle them.
	// You want to make that easy.
	// 'message' is intended to be human readable.
	//
	// The resulting event will be created in the same namespace as the reference object.
	Event(object runtime.Object, eventtype, reason, message string)

	// Eventf is just like Event, but with Sprintf for the message field.
	Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{})

	// PastEventf is just like Eventf, but with an option to specify the event's 'timestamp' field.
	PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{})

	// AnnotatedEventf is just like eventf, but with annotations attached
	AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{})
}

그럼 operator-sdk로 생성한 controller에서 Kubernetes Events를 사용하는 방법을 간단히 살펴 보도록 하겠습니다.

커스텀 리소스의 이름이 Jupyter 라고 가정하겠습니다. 먼저 jupyter_controller.go 파일을 열어서, ReconcileJupyterrecorder record.EventRecorder 을 추가합니다.

// ReconcileJupyter reconciles a Jupyter object
type ReconcileJupyter struct {
	// This client, initialized using mgr.Client() above, is a split client
	// that reads objects from the cache and writes to the apiserver
	client client.Client
	scheme *runtime.Scheme
	recorder record.EventRecorder
}

그런 다음 func newReconcilerEventRecorder 를 생성해서 넘겨줍니다.

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
	return &ReconcileJupyter{client: mgr.GetClient(), scheme: mgr.GetScheme(), recorder: mgr.GetEventRecorderFor("jupyter-controller")}
}

그리고, 필요한 곳에서 EventRecorder를 사용해서, 이벤트를 기록합니다.

import corev1 "k8s.io/api/core/v1"

// Pod event reason list
const (
	CreatedPod				= "CreatedPod"
	FailedToCreatePod			= "FailedToCreatePod"
)

r.recorder.Eventf(jupyter, corev1.EventTypeNormal, CreatedPod, "Created pod : %s", pod.Name)
r.recorder.Eventf(jupyter, corev1.EventTypeWarning, FailedToCreatePod, err.Error())

앞서 설명한 바와 같이, 이벤트를 기록한 후, Jupyer 라는 커스텀 리소스에 대해 세부 내용을 출력해보면, 다음과 같은 결과를 얻을 수 있습니다.

$ kubectl describe elsa
Name:         elsa
Namespace:    snow
...
Events:
  Type    Reason      Age    From                Message
  ----    ------      ----   ----                -------
  Normal  CreatedPod  3s     jupyter-controller  Created pod : jupyter-elsa

지금까지 Kubernetes Events를 사용 방법에 대해 알아봤습니다. 오류 내용을 CR 사용자에게 제공하여 문제를 해결하는데 많은 도움이 될것입니다.

참고

EventRecorder를 직접 생성하는 코드입니다.

import (
	"fmt"
	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes"
	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/record"
)


func NewEventRecorder(config *rest.Config, scheme *runtime.Scheme, logger logr.Logger, name string) (record.EventRecorder, error) {
	clientSet, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, fmt.Errorf("failed to init clientSet: %v", err)
	}

	broadcaster := record.NewBroadcaster()
	broadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: clientSet.CoreV1().Events("")})
	broadcaster.StartEventWatcher(
		func(e *corev1.Event) {
			logger.V(1).Info(e.Type, "object", e.InvolvedObject, "reason", e.Reason, "message", e.Message)
		})

	recorder := broadcaster.NewRecorder(scheme, corev1.EventSource{Component: name})
	return recorder, nil
}

Kubernetes operator-sdk를 이용한 Go Operator 만들기

Install the Operator SDK CLI

Install from Homebrew

$ brew install operator-sdk

Create an operator

작업할 디렉토리를 생성하고, operator-sdk를 사용해서 operator를 생성한다. go 모듈을 사용하기 위해서 GO111MODULE=on을 설정하거나, GOPATH가 아닌 경로에 디렉토리를 생성해야한다.

$ mkdir -p ~/workspace
$ cd ~/workspace
$ export GO111MODULE=on
$ operator-sdk new jupyter-operator --repo github.com/kangwoo/jupyter-operator
INFO[0000] Creating new Go operator 'jupyter-operator'.
INFO[0000] Created go.mod
INFO[0000] Created tools.go
INFO[0000] Created cmd/manager/main.go
INFO[0000] Created build/Dockerfile
INFO[0000] Created build/bin/entrypoint
INFO[0000] Created build/bin/user_setup
INFO[0000] Created deploy/service_account.yaml
INFO[0000] Created deploy/role.yaml
INFO[0000] Created deploy/role_binding.yaml
INFO[0000] Created deploy/operator.yaml
INFO[0000] Created pkg/apis/apis.go
INFO[0000] Created pkg/controller/controller.go
INFO[0000] Created version/version.go
INFO[0000] Created .gitignore
INFO[0000] Validating project
go: finding github.com/operator-framework/operator-sdk master
INFO[0108] Project validation successful.
INFO[0108] Project creation complete.

Mercurial 설치

만일 operator를 생성하는 중 다음과 같이 hg 실행 파일을 찾을 수 없다는 에러가 발생한다면, hg를 별도로 설치해야한다.

$ operator-sdk new jupyter-operator --repo github.com/kangwoo/jupyter-operator
...
go: finding github.com/operator-framework/operator-sdk master
go: bitbucket.org/ww/goautoneg@v0.0.0-20120707110453-75cd24fc2f2c: hg clone -U https://bitbucket.org/ww/goautoneg . in /Users/lineplus/go/pkg/mod/cache/vcs/59c2185b80ea440a7c3b8c5eff3d8abb68c53dea1f20f615370c924c4150b27f: exec: "hg": executable file not found in $PATH
go: error loading module requirements
Error: failed to exec []string{"go", "build", "./..."}: exit status 1
...

hg도 ‘brew’를 사용해서 설치할 수 있다. 참고로 ‘hg’는 [Mercurial]https://www.mercurial-scm.org/ 이라는 크로스 플랫폼 분산 버전 관리 도구의 명령툴이다.

$ brew install hg

생성한 operator 디렉토리 이동

생성한 operator 디렉토리로 이동한다.

$ cd jupyter-operator

CR(Custom Resource) 생성

operator-sdk add api 명령어를 이용해서, API를 생성한다.

$ operator-sdk add api --api-version=kangwoo.github.io/v1alpha1 --kind=Jupyter
INFO[0000] Generating api version kangwoo.github.io/v1alpha1 for kind Jupyter.
INFO[0000] Created pkg/apis/kangwoo/group.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/jupyter_types.go
INFO[0003] Created pkg/apis/addtoscheme_kangwoo_v1alpha1.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/register.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/doc.go
INFO[0003] Created deploy/crds/kangwoo_v1alpha1_jupyter_cr.yaml
INFO[0011] Created deploy/crds/kangwoo_v1alpha1_jupyter_crd.yaml
INFO[0011] Running deepcopy code-generation for Custom Resource group versions: [kangwoo:[v1alpha1], ]
INFO[0019] Code-generation complete.
INFO[0019] Running OpenAPI code-generation for Custom Resource group versions: [kangwoo:[v1alpha1], ]
INFO[0036] Created deploy/crds/kangwoo_v1alpha1_jupyter_crd.yaml
INFO[0036] Code-generation complete.
INFO[0036] API generation complete.

Controller 생성

operator-sdk add controller 명령어를 이용해서, Controller를 생성한다.

$ operator-sdk add controller --api-version=kangwoo.github.io/v1alpha1 --kind=Jupyter
INFO[0000] Generating controller version kangwoo.github.io/v1alpha1 for kind Jupyter.
INFO[0000] Created pkg/controller/jupyter/jupyter_controller.go
INFO[0000] Created pkg/controller/add_jupyter.go
INFO[0000] Controller generation complete.

빌드(Build) 하기

$ operator-sdk build kangwoo/jupyter-operator:latest

코드 생성 하기

리소스의 Spec이 변경되었을 경우, 코드를 다시 생성해줘야한다.

$ operator-sdk generate k8s
$ operator-sdk generate openapi

참고 자료

  • https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md
  • https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md
  • https://github.com/operator-framework/operator-sdk/blob/master/doc/operator-scope.md