Tạo thông báo trên slack khi node được thêm vào Kubernetes

Uncategorized

Nội dung viết về nhóm kỹ sư của ScatterLab, một công ty khởi nghiệp về AI sử dụng công nghệ deep learning để phát triển chatbot AI hỗ trợ tương tác trò chuyện với con người. Họ giới thiệu cách tạo chương trình gửi cảnh báo mỗi lần node được thêm vào mà không cần bên thứ ba sử dụng máy chủ Kubernetes AIP.

Hầu hết các dịch vụ đám mây lớn cung cấp các dịch vụ hình thức quản lý độc lập của riêng họ như Elastic Kubernetes trên AWS hay Google Kubernetes Engine trên GCP. Các dịch vụ này có ưu điểm là có thể triển khai thuận tiện các computing engine được cung cấp bởi bên cloud vendor. Khi dùng Cluster Autoscaler, EC2 (trường hợp EKS) được tự động tạo hoặc xóa theo yêu cầu từ các pod mà không cần quản lý các node group theo cách thủ công.

Nhưng vẫn có lo ngại rằng khi tạo quá nhiều node liệu có lãng phí tiền bạc trong lúc chúng ta không biết hay không. Bằng cách tận dụng Kubernetes API server dù không có các bên liên quan bạn vẫn có thể tạo chương trình đơn giản để gửi cảnh báo khi một node được thêm vào. Điều quan trọng là theo dõi được các sự kiện được ghi lại trên Kubernetes, sau đây chúng tôi sẽ giải thích từng bước từ khái niệm sự kiện đến giai đoạn triển khai sau khi viết code.

Kubernetes event

Chúng tôi dùng lệnh kubectl describe khi muốn kiểm tra trạng thái của cái gì đó đang chạy trên Kubernetes. Tôi sẽ dùng lệnh để xem trạng thái 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

Ở đây điểm chúng ta cần lưu ý là thông tin của sự kiện. Bạn có thể biết được rằng 2 phút 25 giây trước, “container – vùng chứa không thành công đã bắt đầu lại”. Theo như sự kiện này, trên Kubernetes những sự kiện đã xảy ra sẽ được ghi lại. Sự kiện cũng là một loại tài nguyên kubernetes vì vậy bạn có thể gọi thông tin sự kiện bằng định dạng 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"
}

Lưu ý về Event API version

Ví dụ trên thay vì chỉ viết events chúng tôi sẽ nhớ version và nhóm API events.v1.events.k8s.io. Vì trước events v1 core group và sau là Event v1 group events.k9s.io được phân chia rõ ràng. Từ ví dụ này, trường hợp core v1 Event có involed Object field nhưng không thể tìm thấy ở events.k8s.io v1 Event mà thay vào đó là regarding field.

Nội dung này chỉ đang nói về Event API của nhóm events.k8s.io/v1 có thể dùng được từ phiên bản Kubernetes 1.19 trở lên. Dưới đây là một vài giá trị và field quan trọng cấu tạo nên sự kiện trong API này.

  • type: Loại sự kiện Warning(cảnh báo) hay Normal(thông thường) 
  • regarding: Đối tượng Kubernetes được liên kết với sự kiện. Cung cấp các thông tin như loại resource, tên, namespace(nếu có)
  • reason: Giống như một phần nhỏ nguyên nhân gây ra sự kiện này. Nó tương tự NodeReady thường được viết bằng PascalCase và phải nhỏ hơn 128B.
  • note: Mô tả chi tiết hơn về sự kiện xảy ra. Tương tự như tin nhắn báo cáo có thể đọc được của con người và có thể ghi lên đến 1kB theo định nghĩa.

Vui lòng tham khảo tài liệu API chính thức để biết thêm chi tiết

Danh sách các sự kiện liên quan với Autoscaling

Vậy khi thêm node vào bạn có thể tìm thấy sự kiện xảy ra và hoạt động nó. Ngoài ra rất khó để tìm kiếm nội dung nêu rõ trong hoàn cảnh nào loại event nào xảy ra.

Các sự kiện cũng có thể coi như là một số log schema. Đặc điểm API trong Kubernetes đó là chỉ giới hạn trong định dạng của sự kiện, còn thời gian, event nào tạo ra hoàn toàn phụ thuộc vào nhánh Kubernetes component. Nếu bạn muốn biết chính xác danh sách sự kiện xảy ra trong kubernetes cách tốt nhất là kiểm tra kubernetes source code.

Chúng tôi đã chọn một vài sự kiện có thể xảy ra trong quá trình pods và node được autoscaling.

Sự kiện được tạo từ kubelet

Kubelet là đối tượng quản lý pod container trên mỗi đầu node Kubernetes cluster. Khi node được tạo/xóa hay khởi chạy/tắt container kubelet sẽ để lại event.

Tiếp theo regarding.kind là reason của event trong node

  • NodeReady: Xảy ra khi một node chuẩn bị tiếp nhận pod. Xảy ra tối thiểu một lần khi một node được thêm bởi Cluster Autoscaler.
  • NodeNotReady: Xảy ra khi node không ở trạng thái sẵn sàng. Xảy ra tối thiểu một lần khi loại bỏ một node bởi Cluster Autoscaler

Tiếp regarding.kind là reason của event trong Pod. Sự kiện dưới đây xảy ra theo đơn vị container mà không phải trên pod. Ngay cả khi đã tạo một pod nếu nhiều container cấu tạo trong pod đó, các sự kiện với cùng một lý do có thể xảy ra nhiều lần.

  • Created: Xảy ra mỗi khi container của pod được tạo
  • Started: Xảy ra mỗi khi container của pod được khởi động
  • Killing: Xảy ra mỗi khi kết thúc container của pod

Lý do khác hãy tham khảo kubelet/events/event.go

Sự kiện được tạo bởi controller khác

Kubelet trước đó là đối tượng quản lý container, tạo ra các sự kiện theo chu kỳ vòng đời của container. Tương tự giống với ReplicaSet controller quản lý pod tạo sự kiện dựa vào chu kỳ của pod. Sau đó regarding.kind là reason của event trong ReplicaSet

  • SuccessfulCreate: Xảy ra khi ReplicaSet đã thêm một pod
  • SuccessfulDelete: Xảy ra khi ReplicaSet đã xóa một pod

Tiếp theo regarding.kind là lý do của event trong Deployment

  • SaclingReplicaSet: Deployment xảy ra khi đã scale ReplicaSet. Bạn có thể tham khảo note là scale up hay scale down

Regarding.kind là sự kiện HorizontalPodAutoscaler

  • SuccessfulRescale: Xảy ra khi HPA đã thay đổi số lượng Replica deployment.

Có thể biến mất bất cứ khi nào

Kubernetes cluster của môi trường production thường phát ra nhiều sự kiện. Nên sẽ khá nặng nề khi bất cứ sự kiện nào được lưu bán cố định ở trạng thái Kuberenetes lưu trữ(etcd). Vì vậy sự kiện Kubernetes được lưu ở TTL mặc định và giá trị mặc định đó theo phiên bản tiêu chuẩn 1.24 là 1 giờ. Tức, các sự kiện cũ hơn 1 giờ không thể theo dõi bằng kubectl.

Sự kiện chỉ có hiệu lực trong thời gian giới hạn nên sự tồn tại của đối tượng sự kiện không thể được tận dụng cho các hoạt động yêu cầu ở mức độ nhất quán cao. Nhưng trong bài viết lần này, sự kiện sẽ khá hữu dụng để đạt mục đích đơn giản “Gửi thông báo trên slack khi một sự kiện được tạo”.

Theo dõi sự kiện với Watch API

Trung tâm của Kubernetes có Kubernetes API server(còn được gọi là kube-apiserve). Vì nó theo phương thức REST API nên chúng tôi có thể dùng một cách trực quan và đương nhiên có thể sử dụng để lấy đối tượng sự kiên chính trong ngày.

$ 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",

Khi thêm option ?watch=true bạn có thể phát trực tiếp response Body(dữ liệu tài nguyên) từng dòng qua dạng JSON khi một đối tượng mới được tạo hay một đối tượng được tạo bởi kết nối HTTP mà không cần phải thăm dò mọi lúc.

$ 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...

Chúng tôi đã thử dùng ngôn ngữ lập trình Go đọc các sự kiện NodeReady được viết bởi kubelet bằng cách sử dụng API này.

// 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)
    }
  }
}

Nếu thêm Node vào Kubernetes trong khi chạy chương trình Go này chúng tôi có thể kiểm tra được dấu vết của sự kiện trên console.

$ 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

Ngay cả khi đó không phải là ngôn ngữ Go, miễn là bạn có thể xử lý thạo ngôn ngữ lập trình của bạn và thư viện HTTP thì không khó để tạo cảnh báo sự kiện cho riêng mình. Nhưng lệnh ở trên cũng lấy được thông tin của sự kiện trong quá khứ khi gọi API đầu tiên nên để tránh gọi trùng lặp hãy dùng tham số truy vấn resourceVersion hay meta data creationTimestamp của đối tượng sự kiện. 

Triển khai service

Cần so sánh và triển khai tái thiết lập kết nối khi ngắt kết nối HTTP của kubernetes API và sever. Sau khi deployment trên Kubernetes mỗi lần container bị ngắt một cách bất thường Kubernetes sẽ thử dùng chức năng self-healing.

Đầu tiên bạn phải viết Dockerfile để tiếp nhận.

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

Nội dung này chúng tôi đã tóm gọn lại nhưng nếu bạn dùng Multi-stage build viết Dockerfile có thể làm giảm rất nhiều dung lượng của tổng các image bởi tách biệt tính phụ thuộc của build và thực thi.

Tiếp theo triển khai deployment vùng chứa(container). Lúc này Pod phải xác định theo resource ServiceAccount để thông tin trực tiếp với server API kubernetes. Người dùng có ServiceAccount mang những thông tin sự kiện Kubernetes từ bất cứ namespace nào phải xác định được ClusterRole và ClusterRoleBinding. Dưới đây là tất cả nội dung tổng hợp thể hiện bằng 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

Để giao tiếp đơn giản bằng cách kết nối với http://localhost:8001, lệnh kublect proxy đã khởi chạy bằng sidecar container. Các container trong cùng một pod chia sẻ network interface với nhau nên chúng có thể tiếp cận trực tiếp với proxy server được mở trong sidecar container. Cũng có những phương pháp khác xác thực tới máy chủ Kubernetes API server, các bạn hãy tham khảo nội dung chính thức Accessing the Kubernetes API from a Pod

Kết

Chúng ta đã tìm hiểu về Kubernetes event resource và ứng dụng nó tạo chương trình thông báo đến mỗi khi node được thêm vào. Sự kiện có thể được mở rộng bằng những cách đa dạng như gửi tin nhắn qua slack hay tổng hợp lại cả label thông tin của node mỗi khi sự kiện xảy ra.

Ví dụ cảnh báo Kubernetes event

Tuy rất khó hoạt động khi mở hơn hàng trăm pod trên Production Cluster mỗi ngày nhưng nếu dùng Cluster cho quy mô phát triển nhỏ chắc chắn sẽ rất có ích cho bạn khi tạo chương trình thông báo đúng với yêu cầu nghiệp vụ. Thực tế chúng tôi đã phát triển hệ thống tin nhắn slack gửi đi mỗi khi triển khai deployment và nhận được phản hồi rất tốt.

Nội dung bài viết này đặt trọng tâm ở sự kiện nhưng theo nguyên lý tương tự bạn có thể tạo ra chương trình đáp ứng được với thay đổi trạng thái nguồn khác mà không phải là event. Đây gọi là khái niệm Controller trên Kubernetes. Kubernetes business logic như Rolling Update một deployment cơ bản hoạt động dựa trên Controller pattern. Nếu các bạn quan tâm hơn về môi trường kubernetes chúng tôi đề cử cho các bạn học thêm các từ khoá như “custom resource” và “custom controller”.


The translated article above belongs to Scatter Lab Tech Engineering team and Metacoders commits not to use this article for any commercial purposes

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