LabHub
배우기 러닝패스 코스

CRDs and Operators

It is in the logs but the user cannot see it - making your operator speak

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

두 이벤트 API 로 이벤트를 직접 만들어 필수 필드와 집계를 확인하고, 관례를 어겼을 때 도구에서 어떻게 사라지는지 본 뒤, 이벤트만 읽어 사고를 재구성하는 표를 만든다.

왜 중요한가

오퍼레이터가 무슨 일을 했는지 사용자가 알 수 있는 자리는 세 곳뿐이다 — 컨트롤러 로그, 리소스의 status, 그리고 이벤트. 로그는 클러스터 운영자만 보고, status 는 지금 상태만 말할 뿐 과정을 담지 않는다. 사용자가 실제로 읽는 자리는 kubectl describe 아래쪽의 Events 뿐이다. 그래서 오퍼레이터를 만드는 일의 절반은 무엇을 이벤트로 남길지 정하는 일이다. 다만 이벤트는 로그가 아니다. 기본 보존이 한 시간이고, 같은 이유의 반복은 새 오브젝트가 아니라 count 증가로 합쳐지며, API 가 둘인데 저장소는 하나다. 이 성질을 모르고 이벤트에 감사 추적을 기대면 정작 사고를 조사할 때 아무것도 남아 있지 않다.

단계

  1. /root/op-events/pipeline-crd.yaml 에 CRD pipelines.ev.labhub.io 를 쓰세요 — 그룹 ev.labhub.io, 종류 Pipeline, 복수형 pipelines, 버전 v1 하나이고 스키마에는 spec.stage(string) 만 둡니다. 네임스페이스 op-events 를 만들고 /root/op-events/pipeline-build1.yaml 로 Pipeline build-1(stage build)을 적용한 뒤 그 metadata.uid/root/op-events/pipeline-uid.txt 에 한 줄로 저장하세요.
  2. /root/op-events/event-started.yamlv1 Event build-1-started 를 쓰세요 — involvedObject 는 Pipeline build-1(apiVersion·kind·name·namespace·실제 uid), reasonReconcileStarted, typeNormal, message 는 한 문장, firstTimestamplastTimestamp 는 지금 시각, count 는 1, source.componentpipeline-operator 입니다. 적용한 뒤 kubectl -n op-events events 의 출력을 /root/op-events/events-list.txt 에 저장하세요.
  3. /root/op-events/event-bad.yaml 에 Event build-1-orphan 을 쓰되 involvedObject아예 넣지 않습니다(reason NoTarget, type Normal, message 한 문장). 적용을 시도해 결과를 /root/op-events/invalid-event.txt 에 모으세요 — 첫 줄은 apply-rc=<종료 코드> 이고 그 아래에 서버가 낸 문장을 그대로 붙입니다.
  4. /root/op-events/event-weird.yaml 에 Event build-1-weird 를 쓰세요 — 대상은 Pipeline build-1, reasonStageUnknown, 그리고 type 을 관례 밖의 값 Critical 로 적습니다(시각 필드는 2단계와 같은 형식으로 채웁니다). 적용한 뒤 kubectl -n op-events events --types=Criticalkubectl -n op-events events --types=Warning 을 차례로 실행해 결과를 /root/op-events/type-report.txt 에 모으세요 — 두 명령의 출력과 각각의 종료 코드(critical-rc=, warning-rc=)가 들어가야 합니다.
  5. 2단계의 이벤트가 네 번 더 일어났다고 가정하고 집계 필드를 갱신하세요 — kubectl -n op-events patch event build-1-started --type=mergecount 를 5 로, lastTimestamp 를 지금 시각으로 바꿉니다. 그다음 kubectl -n op-events events 의 출력을 /root/op-events/count-report.txt 에 저장하세요.
  6. /root/op-events/event-done.yamlevents.k8s.io/v1 Event build-1-done 을 쓰세요 — 대상은 regarding(Pipeline build-1, 실제 uid 포함), reasonReconcileSucceeded, note 는 한 문장, typeNormal, eventTime 은 지금 시각(소수점 이하 6자리), reportingControllerpipeline-operator, reportingInstancepipeline-operator-0, actionReconcile 입니다. 적용한 뒤 두 API 로 각각 목록을 뽑아 /root/op-events/both-apis.txt 에 저장하세요 — core: 로 시작하는 줄들과 new: 로 시작하는 줄들이 각각 kubectl get eventskubectl get events.events.k8s.io 의 이름 목록이어야 합니다.
  7. /root/op-events/hungry-pod.yaml 에 파드 hungry 를 쓰세요 — 컨테이너 하나(app, 이미지 busybox:1.36)에 requests.cpu"64" 로 요구합니다. 적용하고 스케줄러가 이벤트를 남길 때까지 기다린 뒤 kubectl -n op-events get events --field-selector reason=FailedScheduling -o wide 의 출력을 /root/op-events/scheduler-event.txt 에 저장하세요.
  8. /root/op-events/timeline.sh 를 만드세요 — op-events 의 모든 이벤트를 <type> <reason> <대상종류>/<대상이름> 한 줄씩으로 찍되 정렬하고 중복을 없앱니다. 나이·시각·count 처럼 볼 때마다 달라지는 값은 넣지 않습니다. 출력을 /root/op-events/timeline.txt 에 저장하고, 그 표만 보고 무슨 일이 있었는지 /root/op-events/incident.txt 에 사람이 읽을 문장으로 적으세요 — 스케줄러가 남긴 이유와 오퍼레이터가 남긴 이유가 모두 언급돼야 하고, 관례 밖의 type 이 섞여 있다는 것도 지적해야 합니다.

참고

이벤트를 붙일 대상을 만든다

/root/op-events/pipeline-crd.yaml 에 CRD pipelines.ev.labhub.io 를 쓰세요 — 그룹 ev.labhub.io, 종류 Pipeline, 복수형 pipelines, 버전 v1 하나이고 스키마에는 spec.stage(string) 만 둡니다. 네임스페이스 op-events 를 만들고 /root/op-events/pipeline-build1.yaml 로 Pipeline build-1(stage build)을 적용한 뒤 그 metadata.uid/root/op-events/pipeline-uid.txt 에 한 줄로 저장하세요.

이벤트는 항상 '어떤 오브젝트에 대한 이야기' 입니다. 그래서 대상의 종류·이름·네임스페이스뿐 아니라 uid 까지 적어야 같은 이름으로 다시 만들어진 다른 오브젝트의 이야기와 섞이지 않습니다. uid 는 다음 단계에서 그대로 씁니다.

오퍼레이터가 첫 마디를 남긴다

/root/op-events/event-started.yamlv1 Event build-1-started 를 쓰세요 — involvedObject 는 Pipeline build-1(apiVersion·kind·name·namespace·실제 uid), reasonReconcileStarted, typeNormal, message 는 한 문장, firstTimestamplastTimestamp 는 지금 시각, count 는 1, source.componentpipeline-operator 입니다. 적용한 뒤 kubectl -n op-events events 의 출력을 /root/op-events/events-list.txt 에 저장하세요.

reason 은 사람이 읽는 문장이 아니라 기계가 세는 열쇠입니다. 그래서 관례가 짧은 CamelCase 한 낱말이고, 같은 이유가 반복되면 새 이벤트가 아니라 하나로 합쳐집니다. message 쪽에 자세한 설명을 넣으세요. 대상이 커스텀 리소스라도 kubectl describe 아래쪽에 그대로 붙습니다.

대상 없는 이벤트는 만들 수 없다

/root/op-events/event-bad.yaml 에 Event build-1-orphan 을 쓰되 involvedObject아예 넣지 않습니다(reason NoTarget, type Normal, message 한 문장). 적용을 시도해 결과를 /root/op-events/invalid-event.txt 에 모으세요 — 첫 줄은 apply-rc=<종료 코드> 이고 그 아래에 서버가 낸 문장을 그대로 붙입니다.

이벤트는 대상이 있어야 의미가 생깁니다. 대상이 없으면 어느 오브젝트의 describe 에도 붙지 못하고 목록에서 이유만 떠다니게 됩니다. 그래서 API 가 아예 거부하는데, 오류가 정확히 어느 필드를 지목하는지 읽어 보면 이벤트와 대상의 네임스페이스 관계도 함께 알 수 있습니다.

관례 밖의 type 은 도구에서 사라진다

/root/op-events/event-weird.yaml 에 Event build-1-weird 를 쓰세요 — 대상은 Pipeline build-1, reasonStageUnknown, 그리고 type 을 관례 밖의 값 Critical 로 적습니다(시각 필드는 2단계와 같은 형식으로 채웁니다). 적용한 뒤 kubectl -n op-events events --types=Criticalkubectl -n op-events events --types=Warning 을 차례로 실행해 결과를 /root/op-events/type-report.txt 에 모으세요 — 두 명령의 출력과 각각의 종료 코드(critical-rc=, warning-rc=)가 들어가야 합니다.

API 서버는 type 값을 검사하지 않습니다. 그래서 오브젝트는 멀쩡히 만들어집니다. 문제는 그 뒤인데, 이벤트를 읽는 도구들은 Normal 과 Warning 두 가지만 있다고 전제하고 만들어져 있습니다. 관례가 강제되지 않는데도 지켜야 하는 이유가 여기 있습니다.

같은 이유의 반복은 새 이벤트가 아니다

2단계의 이벤트가 네 번 더 일어났다고 가정하고 집계 필드를 갱신하세요 — kubectl -n op-events patch event build-1-started --type=mergecount 를 5 로, lastTimestamp 를 지금 시각으로 바꿉니다. 그다음 kubectl -n op-events events 의 출력을 /root/op-events/count-report.txt 에 저장하세요.

이벤트 기록기는 같은 대상·같은 이유·같은 메시지를 다시 만나면 새 오브젝트를 만들지 않고 이 두 필드만 고칩니다. 목록 화면의 LAST SEEN 칸이 그때 어떻게 바뀌는지 직접 보세요 — 괄호 안의 표기가 이 단계의 답입니다. 조정 루프가 초당 몇 번씩 돌아도 etcd 가 터지지 않는 이유이기도 합니다.

API 는 둘인데 저장소는 하나다

/root/op-events/event-done.yamlevents.k8s.io/v1 Event build-1-done 을 쓰세요 — 대상은 regarding(Pipeline build-1, 실제 uid 포함), reasonReconcileSucceeded, note 는 한 문장, typeNormal, eventTime 은 지금 시각(소수점 이하 6자리), reportingControllerpipeline-operator, reportingInstancepipeline-operator-0, actionReconcile 입니다. 적용한 뒤 두 API 로 각각 목록을 뽑아 /root/op-events/both-apis.txt 에 저장하세요 — core: 로 시작하는 줄들과 new: 로 시작하는 줄들이 각각 kubectl get eventskubectl get events.events.k8s.io 의 이름 목록이어야 합니다.

새 API 는 필드 이름이 다릅니다 — involvedObject 가 regarding 으로, message 가 note 로, source 가 reportingController 와 reportingInstance 로 나뉘었습니다. 그런데 저장되는 자리는 같아서 옛 API 로도 그대로 보입니다. 옛 API 로 볼 때 어떤 칸이 비어 있는지 확인해 보면 두 스키마의 차이가 눈에 들어옵니다.

진짜 컨트롤러가 남기는 이벤트를 받아 본다

/root/op-events/hungry-pod.yaml 에 파드 hungry 를 쓰세요 — 컨테이너 하나(app, 이미지 busybox:1.36)에 requests.cpu"64" 로 요구합니다. 적용하고 스케줄러가 이벤트를 남길 때까지 기다린 뒤 kubectl -n op-events get events --field-selector reason=FailedScheduling -o wide 의 출력을 /root/op-events/scheduler-event.txt 에 저장하세요.

이 파드는 어느 노드에도 들어갈 수 없습니다. 스케줄러는 그 사실을 파드의 status 에만 적는 것이 아니라 이벤트로도 남기는데, 메시지에 '몇 개 중 몇 개가 왜 안 되는지' 가 그대로 들어 있습니다. 사람이 원인을 찾을 때 실제로 읽는 문장이 이것입니다. 이벤트가 붙기까지 몇 초 걸리니 조건 반복문으로 기다리세요.

이벤트만 읽어 사고를 재구성한다

/root/op-events/timeline.sh 를 만드세요 — op-events 의 모든 이벤트를 <type> <reason> <대상종류>/<대상이름> 한 줄씩으로 찍되 정렬하고 중복을 없앱니다. 나이·시각·count 처럼 볼 때마다 달라지는 값은 넣지 않습니다. 출력을 /root/op-events/timeline.txt 에 저장하고, 그 표만 보고 무슨 일이 있었는지 /root/op-events/incident.txt 에 사람이 읽을 문장으로 적으세요 — 스케줄러가 남긴 이유와 오퍼레이터가 남긴 이유가 모두 언급돼야 하고, 관례 밖의 type 이 섞여 있다는 것도 지적해야 합니다.

사고 조사에서 이벤트가 값어치를 갖는 이유는 '누가 무엇에 대해 무슨 판단을 했는가' 가 한 줄씩 남아 있기 때문입니다. 그런데 기본 보존이 한 시간이라 늦게 들어가면 아무것도 없습니다. 그래서 오래 보관해야 할 것은 이벤트가 아니라 status 의 조건이나 바깥 저장소로 보낸 기록이어야 합니다.