Перейти до змісту

Kubernetes API Server за 300 рядків

Kubernetes API Server за 300 рядків

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Kubernetes API Server - центр всього кластера. Через нього проходять всі запити: створити под, подивитись стан, видалити deployment. У Shepherd ми побудували його за ~300 рядків ключового коду (520 з хелперами і хендлерами для всіх ресурсів), використовуючи тільки стандартну бібліотеку net/http.

Структура

type APIServer struct {
    store     *Store
    scheduler *Scheduler
    server    *http.Server
    logger    *log.Logger
}

API Server не містить бізнес-логіки. Він приймає HTTP запити, валідує, записує в Store і повідомляє інші компоненти.

Реєстрація маршрутів

func NewAPIServer(addr string, store *Store,
    scheduler *Scheduler, logger *log.Logger) *APIServer {

    mux := http.NewServeMux()

    mux.HandleFunc("/api/v1/pods", api.handlePods)
    mux.HandleFunc("/api/v1/namespaces/",
        api.handleNamespacedResource)
    mux.HandleFunc("/api/v1/nodes", api.handleNodes)
    mux.HandleFunc("/api/v1/nodes/", api.handleNode)
    mux.HandleFunc("/api/v1/services", api.handleServices)
    mux.HandleFunc("/api/v1/deployments", api.handleDeployments)
    mux.HandleFunc("/api/v1/events", api.handleEvents)
    mux.HandleFunc("/healthz", func(w http.ResponseWriter,
        r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })
    mux.HandleFunc("/api/v1/info", api.handleInfo)

    api.server = &http.Server{
        Addr:    addr,
        Handler: api.logging(mux),
    }
    return api
}

URL-и схожі на справжній Kubernetes: /api/v1/pods, /api/v1/namespaces/{ns}/pods/{name}.

Namespaced ресурси

func (api *APIServer) handleNamespacedResource(
    w http.ResponseWriter, r *http.Request) {
    // /api/v1/namespaces/{ns}/pods/{name}
    parts := strings.Split(
        strings.TrimPrefix(r.URL.Path,
            "/api/v1/namespaces/"), "/")

    ns := parts[0]
    resource := parts[1]

    switch resource {
    case "pods":
        if len(parts) == 2 {
            // Список подів в namespace
            api.listPods(w, r, ns)
        } else {
            name := parts[2]
            switch r.Method {
            case http.MethodGet:
                api.getPod(w, r, ns, name)
            case http.MethodPut:
                api.updatePod(w, r, ns, name)
            case http.MethodDelete:
                api.deletePod(w, r, ns, name)
            }
        }
    case "services": // ...
    case "deployments": // ...
    }
}

Створення Pod

Ось де відбувається найцікавіше:

func (api *APIServer) createPod(w http.ResponseWriter,
    r *http.Request) {
    var pod Pod
    json.NewDecoder(r.Body).Decode(&pod)

    // Заповнюємо дефолти
    pod.Kind = "Pod"
    if pod.Metadata.Namespace == "" {
        pod.Metadata.Namespace = "default"
    }
    if pod.Metadata.UID == "" {
        pod.Metadata.UID = generateUID()
    }
    pod.Metadata.CreatedAt = time.Now()
    pod.Status.Phase = PodPending  // Завжди Pending!

    api.store.CreatePod(&pod)

    // Записуємо подію
    api.store.RecordEvent(Event{
        Type:    "Normal",
        Reason:  "Created",
        Message: fmt.Sprintf("Pod %s created",
            pod.Metadata.Name),
        Object: "pod/" + pod.Metadata.Name,
    })

    // Запускаємо планування асинхронно
    go api.scheduler.SchedulePod(&pod)

    respondJSON(w, http.StatusCreated, pod)
}

Дивись, яка штука: под завжди створюється як Pending. Навіть якщо є вільні ноди. Планування відбувається асинхронно через go api.scheduler.SchedulePod(). Це та сама модель, що і в справжньому Kubernetes.

Middleware для логування

func (api *APIServer) logging(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            next.ServeHTTP(w, r)
            api.logger.Printf("%s %s %s",
                r.Method, r.URL.Path, time.Since(start))
        })
}
graph LR
    REQ["HTTP Request"] --> LOG["Logging middleware"]
    LOG --> MUX["ServeMux routing"]
    MUX --> H["Handler"]
    H --> STORE["BoltDB Store"]
    H --> RESP["JSON Response"]

Хелпери

func respondJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func generateUID() string {
    b := make([]byte, 16)
    rand.Read(b)
    return fmt.Sprintf("%x-%x-%x-%x-%x",
        b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}

UID генерується у форматі, схожому на UUID. Не RFC 4122 сумісний, але для ідентифікації достатньо.

Що не реалізовано

У справжньому Kubernetes API Server є:

  • Admission webhooks (валідація і мутація)
  • RBAC (авторизація)
  • Watch API (long polling для змін)
  • CRD (Custom Resource Definitions)
  • API versioning і conversion
  • etcd як бекенд (у нас BoltDB)

Але суть та сама: REST API, CRUD операції, асинхронні контролери. Цього достатньо, щоб зрозуміти архітектуру Kubernetes.

Нюанс

Немає валідації вхідних даних. Можна створити под без containers, deployment з від'ємним replicas, service без портів. У Kubernetes admission webhooks і вбудована валідація це ловлять. У Shepherd - garbage in, garbage out.

Спробуй сам

# Запусти Shepherd в standalone mode:
sudo ./shepherd --mode standalone --addr :9876
# В іншому терміналі:
curl -s localhost:9876/healthz
curl -s localhost:9876/api/v1/info | jq .
curl -s localhost:9876/api/v1/pods | jq .

API працює. Далі - BoltDB: як embedded база зберігає весь стан кластера.

Серія: Оркестратор (Shepherd = Kubernetes)

  1. Linux Namespaces | 2. Re-Exec Pattern | 3. pivot_root | 4. Cgroups v2 | 5. OverlayFS | 6. Bridge Networking | 7. NAT і iptables | 8. Image Management | 9. Container Lifecycle | 10. Docker CLI | 11. Kubernetes API Server (ця стаття)

Ресурси

Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow

Попередня: Docker CLI