흔히 오퍼레이터(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
를 생성해야합니다. EventRecorder
는 controller-runtime
의 manager
를 이용해서 쉽게 생성할 수 있습니다.
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
파일을 열어서, ReconcileJupyter
에 recorder 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 newReconciler
에 EventRecorder
를 생성해서 넘겨줍니다.
// 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 }