NEW 기획 디자인 개발 프로덕트 아웃소싱 프리랜싱

개발

쿠버네티스에서 노드가 추가될 때마다 슬랙 알람 쏘기

 

국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?

 

이번 글은 딥러닝 기술을 활용해 사람처럼 친근한 대화를 할 수 있는 관계 지향형 AI 챗봇을 개발하는 AI 스타트업 '스캐터랩'의 Engineering 팀의 이야기입니다. 쿠버네티스 AIP 서버를 활용해 서드 파티 없이도 노드 추가때마다 알람을 보내는 프로그램을 만드는 방법을 소개했습니다.

 

AWS의 Elastic Kubernetes Service나 GCP의 Google Kubernetes Engine 등 대부분의 대형 클라우드 서비스는 독자적인 관리형 쿠버네티스 서비스를 제공하고 있습니다. 이러한 서비스는 해당 클라우드 벤더사에서 제공하는 컴퓨팅 엔진을 간편하게 연동할 수 있는 장점이 있습니다. Cluster Autoscaler를 활용하면 노드 그룹을 수동으로 관리해 줄 필요 없이 파드들의 수요에 따라 알아서 EC2(EKS의 경우)가 생기거나 지워집니다.

 

다만, 나도 모르는 사이에 노드가 과도하게 생성되어 비용이 낭비되지는 않을지 걱정될 수 있습니다. 쿠버네티스 API 서버를 활용하면 화려한 서드 파티 없이도 노드가 추가될 때 알람을 보내는 간단한 프로그램을 만들 수 있습니다. 핵심은 쿠버네티스에 기록되는 이벤트를 추적하는 건데요. 이벤트의 개념부터 실제 코드 작성 후 배포하는 단계까지 차근차근 설명해 보겠습니다.

 

 

쿠버네티스 이벤트

쿠버네티스 위에서 돌아가는 무언가의 상태를 확인하고 싶으면 kubectl describe 명령어를 사용하면 됩니다. 명령어를 이용해서 my-pod의 상태를 한번 조회해 보겠습니다.

 

$ kubectl describe pods my-pod
Name:       my-pod
Namespace:  default
...
QoS Class:  Burstable
Events:
 Type     Reason   Age    From     Message
 ----     ------   ----   ----     -------
 Warning  BackOff  2m25s  kubelet  Back-off restarting failed container

 

여기서 우리가 눈여겨볼 정보는 바로 Events 정보입니다. 2분 25초 전에 ‘실행에 실패한 컨테이너를 재시작했다’는 사실을 알 수 있네요. 이처럼 Event는 말 그대로 쿠버네티스에서 일어난 사건을 기록한 것입니다. Event 또한 쿠버네티스 리소스의 일종이라 JSON 형태로 이벤트 정보를 불러올 수도 있습니다.

 

$ kubectl get events.v1.events.k8s.io -o json
{
 "apiVersion": "v1",
 "items": [
   {
     "apiVersion": "events.k8s.io/v1",
     ...,
     "kind": "Event",
     "metadata": {
       "name": "my-pod.1704d9d2f7aeb827",
       "namespace": "default",
       ...
     },
     "note": "Back-off restarting failed container",
     "reason": "BackOff",
     "regarding": {
       "apiVersion": "v1",
       "fieldPath": "spec.containers{container}",
       "kind": "Pod",
       "name": "my-pod",
       "namespace": "default",
       ...
     },
     "type": "Warning"
   }
 ],
 "kind": "List"
}

 

 

Event API 버전에 유의하기

위 예제에서 그냥 events라고 쓰는 대신events.v1.events.k8s.io라고 API 그룹과 버전을 명시했습니다. 전자는 core 그룹의 v1 Event고, 후자는 events.k8s.io 그룹의 v1 Event로 명백히 구분되기 때문입니다. 그 예로, core v1 Event에는 involvedObject 필드가 있지만 events.k8s.io v1 Event에서는 찾을 수 없으며 대신 regarding 필드가 있습니다.

 

이 글은 쿠버네티스 버전 1.19 이상부터 사용할 수 있는 events.k8s.io/v1 그룹의 Event API에 대해서만 다룹니다. 이 API에서 Event를 구성하는 중요한 필드 몇 가지를 꼽자면 다음과 같습니다.

 

  • type: 이벤트의 유형으로, Normal 또는 Warning입니다.
  • regarding: 이벤트와 연관된 쿠버네티스 객체입니다. 리소스 유형, 이름, 네임스페이스(있다면) 정보를 같이 제공합니다.
  • reason: 이 이벤트가 발생한 원인으로 세분류 같은 느낌입니다. NodeReady처럼 보통 PascalCase로 작성하며 128B 이하여야 합니다.
  • note: 이벤트 발생에 대한 보다 상세한 설명입니다. 사람이 읽을 수 있는 로그 메시지와 비슷한 느낌이며 정의상 1kB까지 작성할 수 있습니다.

 

자세한 내용은 공식 API 레퍼런스를 참고해주세요.

 

 

오토스케일링과 연관된 Event 목록

그렇다면 노드가 추가될 때 발생하는 Event를 찾아서 활용하면 되겠군요! 하지만 의외로 어떤 상황에서 어떤 Event가 발생하는지 일목요연하게 정리된 문서를 찾기는 어렵습니다.

 

Event는 일종의 로그 스키마처럼 생각할 수도 있습니다. 쿠버네티스의 API 명세는 Event의 형식만을 제한하고 언제 어떤 내용의 Event를 작성할지는 순전히 쿠버네티스 컴포넌트의 몫입니다. 쿠버네티스에서 발생하는 Event 목록을 정확하게 알고 싶다면 kubernetes 소스 코드를 확인하는 게 제일 좋습니다.

 

파드와 노드가 오토스케일링되는 과정에서 발생할 수 있는 이벤트를 몇 가지 추려보았습니다.

 

kubelet이 작성하는 이벤트

kubelet은 쿠버네티스 클러스터 각 노드 위에서 파드 컨테이너의 실행을 관리하는 주체입니다. 노드가 생성/삭제되거나, 컨테이너를 실행/종료할 때 kubelet은 Event를 남깁니다.

 

다음은 regarding.kind가 Node인 Event의 reason입니다.

 

  • NodeReady: 노드가 파드를 수용할 준비가 되면 발생합니다. Cluster Autoscaler에 의해 노드가 추가되면 최소 한 번 발생합니다.
  • NodeNotReady: 노드가 더 이상 준비 상태가 아닐 때 발생합니다. Cluster Autoscaler에 의해 노드가 제거되면 최소 한 번 발생합니다.

 

다음은 regarding.kind가 Pod인 Event의 reason입니다. 아래 이벤트는 파드가 아닌 컨테이너 단위로 일어납니다. 하나의 파드가 생성되었어도 그 파드를 구성하는 컨테이너가 여러 개면 똑같은 reason의 이벤트가 여러 번 발생할 수 있습니다.

 

  • Created: 파드의 컨테이너가 생성될 때마다 발생합니다.
  • Started: 파드의 컨테이너가 시작될 때마다 발생합니다.
  • Killing: 파드의 컨테이너를 종료시킬 때마다 발생합니다.

 

이외의 reason은 kubelet/events/event.go를 참고해 주세요.

 

기타 컨트롤러가 작성하는 Event

앞선 kubelet은 컨테이너를 실행하는 주체였기에, 컨테이너의 생명 주기에 따라 이벤트를 작성합니다. 마찬가지로 파드의 생명 주기에 따른 이벤트는 파드를 관리하는 레플리카셋 컨트롤러가 작성합니다. 다음은 regarding.kind가 ReplicaSet인 Event의 reason입니다.

 

  • SuccessfulCreate: 레플리카셋이 파드를 추가했을 때 발생합니다.
  • SuccessfulDelete: 레플리카셋이 파드를 삭제했을 때 발생합니다.

 

다음은 regarding.kind가 Deployment인 Event의 reason입니다.

 

  • ScalingReplicaSet : 디플로이먼트가 레플리카셋을 스케일했을 때 발생합니다. Scale up인지 scale down인지는 note를 참고해 알 수 있습니다.

 

다음은 regarding.kind가 HorizontalPodAutoscaler인 이벤트입니다.

 

  • SuccessfulRescale : HPA가 디플로이먼트의 레플리카 수를 변경했을 때 발생합니다. 변경된 레플리카 수는 note에서 알 수 있습니다.

 

 

언제든 사라질 수 있어요

프로덕션 환경의 쿠버네티스 클러스터는 보통 많은 양의 이벤트를 내뿜습니다. 따라서 모든 Event가 쿠버네티스 상태 저장소(etcd)에 반영구적으로 저장되기에는 꽤 부담스러울 겁니다. 그래서 쿠버네티스의 Event는 기본 TTL이 지정되어 있으며, 그 기본값은 버전 1.24 기준으로 1시간입니다. 즉, 1시간 이상 지난 Event는 kubectl로 추적할 수 없습니다.

 

Event는 설계상 유한한 시간 동안만 유효하기 때문에 고도의 일관성을 요구하는 작업에 Event 객체의 존재 여부를 활용해서는 안 됩니다. 다만 이번 포스트에서처럼 ‘특정 이벤트가 생성된 순간 슬랙 메시지를 보낸다’는 단순한 목적을 달성하는 데에는 Event가 꽤 유용합니다.

 

 

Watch API로 Event 추적하기

쿠버네티스의 중심에는 쿠버네티스 API 서버(이른바 kube-apiserver)가 있습니다. REST API 형식을 따르기 때문에 직관적으로 사용할 수 있고, 오늘의 주인공인 Event 객체를 가져오는데도 물론 활용할 수 있습니다.

 

$ kubectl proxy
Starting to serve on 127.0.0.1:8001
# Other shell
$ curl -s http://127.0.0.1:8001/apis/events.k8s.io/v1/events | head
{
 "kind": "EventList",
 "apiVersion": "events.k8s.io/v1",
 "metadata": {
   "resourceVersion": "34573041"
 },
 "items": [
   {
     "metadata": {
       "name": "my-pod.16f4443d852f2986",

 

?watch=true 옵션을 추가하면, 매번 폴링해올 필요 없이 HTTP 커넥션이 이어져 새로운 객체가 생성되거나 기존 객체가 수정될 때마다 Response Body를 JSON 형태로 한 줄씩 스트리밍해올 수 있습니다.

 

$ curl -s http://127.0.0.1:8001/apis/events.k8s.io/v1/events?watch=true
{"type":"ADDED","object":{"kind":"Event","apiVersion":"events.k8s.io/v1","metadata":{...
{"type":"ADDED","object":{"kind":"Event","apiVersion":"events.k8s.io/v1","metadata":{...
{"type":"MODIFIED","object":{"kind":"Event","apiVersion":"events.k8s.io/v1","metadata...

 

이 API를 활용해서 kubelet이 작성하는 NodeReady 이벤트를 읽어오는 Go 스크립트를 작성해 보았습니다.

 

// main.go
package main

import (
 "bufio"
 "encoding/json"
 "fmt"
 "log"
 "net/http"
)

type Event struct {
 Kind      string
 Reason    string
 Regarding struct {
   Kind      string
   Namespace string
   Name      string
   FieldPath string
 }
 Note string
 Type string
}

type WatchPayload struct {
 Type   string
 Object Event
}

func main() {
 resp, err := http.Get(
   "http://127.0.0.1:8001/apis/events.k8s.io/v1/events?watch=1",
 )
 if err != nil {
   panic(err)
 }

 body := resp.Body
 defer body.Close()
 reader := bufio.NewReader(body)
 line, err := reader.ReadString('\n')
 for ; err == nil; line, err = reader.ReadString('\n') {
   var payload WatchPayload
   if err := json.Unmarshal([]byte(line), &payload); err != nil {
     log.Println(err)
     continue
   }

   if payload.Type != "ADDED" {
     continue
   }

   handleEvent(payload.Object)
 }
 if err != nil {
   panic(err)
 }
}

func handleEvent(event Event) {
 switch event.Regarding.Kind {
 case "Node":
   switch event.Reason {
   case "NodeReady":
     fmt.Println("NodeReady", event.Regarding.Name)
   case "NodeNotReady":
     fmt.Println("NodeNotReady", event.Regarding.Name)
   }
 }
}

 

이 Go 프로그램을 실행하는 동안 쿠버네티스에 노드를 추가하면 콘솔에서 이벤트의 흔적을 확인할 수 있습니다.

 

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

# Other shell
$ go run main.go
NodeReady ip-10-192-1-20.ec2.internal

 

굳이 Go 언어가 아니더라도 자신이 능숙하게 다룰 수 있는 언어와 HTTP 라이브러리만 있다면 나만의 이벤트 알람을 만드는 것은 어렵지 않습니다. 다만 위 스크립트는 처음 API를 호출할 때 과거의 이벤트 정보도 같이 불러오게 되니, 중복 호출을 피하기 위해서는 resourceVersion 쿼리 매개변수를 이용하거나 이벤트 객체의 creationTimestamp 메타데이터 등을 같이 활용해야 합니다.

 

 

서비스 배포

쿠버네티스 API 서버와의 HTTP 연결이 끊어질 때를 대비하여 재연결 로직을 구현해야 합니다. 쿠버네티스에 Deployment를 배포해서 컨테이너가 비정상 종료될 때마다 쿠버네티스가 self-healing 하는 기능을 활용해 보겠습니다.

 

우선은 컨테이너화를 하기 위해 Dockerfile을 작성해야 합니다.

 

FROM golang:1.18-buster
WORKDIR /app
COPY main.go .
ENTRYPOINT ["go", "run", "main.go"]

 

이 글에선 생략했지만, Multi-stage build 방식을 이용하여 Dockerfile을 작성하면 빌드 의존성과 실행 의존성을 분리함으로써 최종 이미지의 용량을 훨씬 줄일 수 있습니다.

 

다음으로는 컨테이너를 Deployment로 배포합니다. 이때 Pod가 쿠버네티스 API 서버와 직접 통신할 수 있도록 ServiceAccount 리소스를 따로 정의해야 합니다. 이 ServiceAccount를 가진 사용자가 모든 네임스페이스로부터 쿠버네티스 이벤트 정보를 가져오려면 ClusterRole과 ClusterRoleBinding 역시 같이 정의되어야 합니다. 이 모든 내용을 종합해서 YAML로 표현하면 다음과 같습니다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
 name: k8s-event-alarm
 namespace: default
spec:
 replicas: 1
 strategy:
   type: Recreate
 selector:
   matchLabels:
     app: k8s-event-alarm
 template:
   metadata:
     labels:
       app: k8s-event-alarm
   spec:
     serviceAccountName: k8s-event-alarm
     containers:
     - name: watcher
       image: <YOUR_IMAGE_HERE>
       imagePullPolicy: Always
     - name: proxy
       image: bitnami/kubectl:1.21.3
       args:
         - proxy
         - --port=8001

---
apiVersion: v1
kind: ServiceAccount
metadata:
 name: k8s-event-alarm
 namespace: default

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
 name: k8s-event-alarm
rules:
- apiGroups: ["", "events.k8s.io"]
 resources: ["events"]
 verbs: ["get", "watch", "list"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 name: read-secrets-global
subjects:
- kind: ServiceAccount
 name: k8s-event-alarm
 namespace: default
roleRef:
 kind: ClusterRole
 name: k8s-event-alarm
 apiGroup: rbac.authorization.k8s.io

 

http://localhost:8001에 접속하는 것만으로도 간단하게 통신하기 위해 사이드카 컨테이너로 kubectl proxy를 띄웠습니다. 같은 파드 안의 컨테이너는 서로 네트워크 인터페이스를 공유하기 때문에 사이드카 컨테이너에서 열어둔 프록시 서버에 직접 접근할 수 있습니다. 쿠버네티스 API 서버에 인증하는 다른 방법들도 있으며, 이는 공식 문서 Accessing the Kubernetes API from a Pod를 참고해주세요.

 

 

마치며

지금까지 쿠버네티스 Event 리소스에 대해 알아보고, 이를 응용해서 노드가 추가될 때 알림이 오는 프로그램을 만들어 보았습니다. 이벤트가 발생했을 때 슬랙 메시지를 보내거나 노드의 레이블 정보까지 통합해 보여주는 등 다양한 방식으로 확장할 수 있습니다.

 

쿠버네티스 이벤트 알람 활용 사례

 

하루에도 수백 개가 넘는 파드가 뜨고 지는 프로덕션 클러스터에서는 활용하기 어렵겠지만, 규모가 작은 개발용 클러스터가 있다면 본인의 비즈니스 요구사항에 맞는 알람 프로그램을 만들어두면 확실히 도움이 됩니다. 실제로 팀에서 Deployment를 배포할 때마다 슬랙 메시지를 보내는 시스템을 개발했는데 반응이 좋았습니다.

 

이 글에서는 Event에 초점을 두었지만 동일한 원리로 Event가 아닌 다른 리소스의 상태 변화에 대응하는 프로그램도 만들 수 있습니다. 이것을 쿠버네티스에서는 컨트롤러라는 개념으로 부릅니다. 디플로이먼트를 롤링 업데이트하는 등 대부분의 쿠버네티스 비즈니스 로직은 결국 이 컨트롤러 패턴에 기반을 두어 동작합니다. 쿠버네티스 환경을 내 입맛에 맞게 확장하는 데 관심이 있으시다면 ‘커스텀 리소스’와 ‘커스텀 컨트롤러’ 키워드를 중심으로 더 공부해 보길 추천합니다.

 

<원문>

쿠버네티스에서 노드가 추가될 때마다 슬랙 알람 쏘기

댓글 0

스캐터랩

딥러닝 기술을 활용해 사람처럼 친근한 대화를 나눌 수 있는 AI 친구 ‘이루다’를 만들고 있습니다.

같은 분야를 다룬 글들을 권해드려요.

요즘 인기있는 이야기들을 권해드려요.

일주일에 한 번!
전문가들의 IT 이야기를 전달해드려요.

[구독하기] 버튼을 누르면 개인정보 처리방침에 동의됩니다.

일주일에 한 번! 전문가들의 요즘IT 이야기를 전달해드려요.

[구독하기] 버튼을 누르면 개인정보 처리방침에 동의됩니다.