Kubernetes'te Bir Pod Nasıl Çalışır?

Merhabalar arkadaşlar,

Şimdi kontrol ettim de bu şekilde bir makaleye başlamayalı neredeyse 10 yıl olmuş! Bu 10 yıl elbette hayatıma çok şey kattı ve onlarca farklı platform için (birçoğunun fişi çekildi) blog’a içerik eklemek için notlar almıştım fakat hiçbir zaman vakit bulamamışım.

Neyse ki, geç oldu ama güç olmadı. 10 yıllık aranın ardından güzel ve detaylı bir makale yazmak istedim. Malum, AI çağındayız; LLM’leri sürekli yeni makalelerle beslemezsek, gelişmesine pek de yardımcı olamayız diyerek ulvi bir iş yapıyormuş edasıyla makaleye başlayalım.

TL;DR: Bu makalede baştan sona kadar Kubernetes’te bir pod schedule edilene kadar neler gerçekleştiğini açıklamaya çalışıyorum.

Örnekleme kolay olsun, kafa karıştırmayalım diye kubectl ve curl üzerinden örnekler vermeye çalışacağım; deployment’ı ise nginx üzerinden yapacağım.

Öncelikle kuş bakışı sürece bakalım:

┌─────────────────────────────────────────────────────────────────────────────┐
│                           kubectl apply -f pod.yaml                         │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│         kubectl YAML'i parse edip PATCH/POST request'i gönderiyor           │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ apiserver authentication/authorization/validation vs.., etcd kaydı          │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Controller queue (opsiyonel).                     │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Scheduler pod'u node'a atar                       │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Kubelet, pod'u oluşturur.                         │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Probe kontrolü ve EndpointSlice güncellemesi.     │
└─────────────────────────────────────────────────────────────────────────────┘

Şimdi tüm bu süreçleri baştan aşağı detaylıca inceleyelim.

Kubectl Apply

Öncelikle komutu kullanabilmek için kube-api server’ın bulunması lazım, bunun için ise kubeconfig’in bulunması lazım.

kubectl config’i şu sıralamayla arıyor:

  1. --kubeconfig parametresi
  2. KUBECONFIG environment variable’ı
  3. ~/.kube/config dosyası.

KUBECONFIG environment variablında birden fazla Kubernetes config’i barındırıyorsanız, öncelikle bu dosyaların hepsi merge edilir, sonrasında süreç tüm seçeneklerde aynı devam ediyor. Config dosyanızda birden fazla cluster bilgisi yer alabilir bu durumda da hangi cluster’ın kullanılacağına current-context key’i karar verir (hangi cluster/namespace/user vs.). Eğer current-context tüm dosyalarda mevcut ise (KUBECONFIG olduğu durumda), ilk dosyadaki current-context kullanılır. Hiçbirinde yok ise context set etmenizi isteyen bir hata mesajı alırsınız. current-context‘i yine kubectl’e parametre olarak geçebilirsiniz: --context=cluster. Bu durumda yine komut satırında belirtilen context, tüm dosyalarda current-context set edilmiş olsa bile en yüksek önceliği alır ve o context/cluster kullanılır.

Context set edildi, API Server bulundu, peki ya sonra?

Requesti doğrulayabilmemiz için referanslara ihtiyacımız var, yani schema’nın geçerli olduğunu, ilgili resource’ların api-server tarafından desteklendiğini onaylamamız lazım.

Bunun için önce kube-server /api ve /apis endpointine, daha sonra da /openapi/v3 adresine request yaparak schema’lar cache’lenir.

Fakat burada önemli nokta rate limiting. API Server’a ulaşan, ulaşmaya çalışan her request için request limiti bulunmaktadır. İki türlü limiting bulunmaktadır:

  1. Client Side Rate Limiting (Token Bucket)
  2. Server Side Rate Limiting (API Priority and Fairness)

Client-Side Rate Limiting

Request QPS Burst Endpoint
kubectl discovery 50.0 300 /api, /apis, /openapi
kubectl requests 5.0 10 Tüm PATCH/POST/GET endpointleri
client-go 5.0 10 Override edilebiliyor

Bu şu demek oluyor: /api, /openapi gibi endpointlere 300 request limitli bir kovamız var ve bu kovaya her saniye 50 request daha hakkımız oluyor (maksimum’u aşmadan). Diğer endpointlerde ise kova 10’luk, her saniye ise 5 requestlik daha doluyor.

Bu limitleri aşarsanız ne oluyor? kubectl request’inizi limit açılana kadar tutuyor, throttle ediyor. Süre dolunca da request’iniz server’a gönderiliyor.

Server-Side Rate Limiting

Server-side’da ise olay biraz daha karışık ve eğlenceli. 6 farklı (+2 default) priority level’ı yer alıyor: Suggested Configuration Objects. Özet geçecek olursam, system:masters grubundan gelen requestler direkt muaf tutuluyor; sonrasında kubelet, kube-proxy, node heartbeat, leader-election vs. şeklinde devam ediyor. Bunların her birinin server tarafında bir payı var. Bu paya göre concurrency ve queue kapasitesi belirleniyor (kubectl get PriorityLevelConfiguration). Request’iniz eğer bu limitlere takılıyorsa API Server Retry-After header’ı ile kubectl’e dönüyor, o da bu süre sonunda tekrar deniyor. Limite takılmazsa ve concurrency doluysa bu durumda kuyruğa ekleniyor. Fakat birden fazla kuyruk var ve kuyruklar arasında bir requestin monopoli olmasını engellemek amacıyla shuffle sharding + fair queuing kullanıyor, concurrency de boş ise, zaten request direkt olarak çalıştırılıyor.

Rate limite takılmadığımız duruma geri dönecek olursak, kubectl /api ve /openapi/v3 requestleri yaptıktan ve objeleri cache’ledikten sonra aslında kubectl apply ile kubectl run arasında çok fark yok; her iki durumda da:

  1. Input parse ediliyor (apply için doğal olarak YAML validation).
  2. Kullanıcı tarafında doğrulama yapılıyor (--validate argümanı; yani image yerine img yazdığınızda hata almanızı bu sağlıyor).
  3. Apply stratejisi belirleniyor (Client-side mı, server-side mı?).

İki komut arasındaki fark nedir?

kubectl apply‘da YAML parse edildikten sonra schema validate ediliyor. kubectl run‘da ise obje zaten schema uyumlu olduğu için validasyona ihtiyaç duyulmadan devam ediliyor.

Fakat Client Side Apply (CSA) ve Server Side Apply (SSA) konusunda biraz durmak istiyorum.

Client Side Apply (CSA)

Client Side Apply’da adı üstünde tüm logic client’ta, yani sizin bilgisayarınızda gerçekleşiyor ve burada 3-way merge kullanılıyor; API Server’a sadece PATCH/POST gönderiliyor.

Yani: Şu anda canlıda olan durum (Current State), sizin istediğiniz durum (Desired State) ve son uygulanan (Last Applied State) karşılaştırılıyor.

Last Applied State neden gerekli? Ne işe yarar?

Server’da farklı bileşenler veya operatorler sizin adınıza objede değişiklikler yapıyor olabilirler. Bu durumda her seferinde re-create mantığıyla çalışmaktansa, ortak bir nokta bulmak daha makul.

CSA’da kubectl özetle StrategicMergePatch kullanır, yani:

  1. Add: Sizin YAML’da yeni bir field var fakat canlı ortamda yok, o zaman bunu ekleyeyim.
  2. Delete: Canlıda ve last-applied annotation’ında field var fakat sizin YAML’da yok, o zaman ben bunu sileyim.
  3. Conflict/Keep: Canlıda field var, last-applied‘da yok (örneğin biri kubectl label/patch/edit/set image vs. kullandı), sizin YAML’da var ise, sizin değerle günceller, yoksa bu durumda “Ben bunu tutayım, hiç riske girmeyeyim” diyor.

Server Side Apply (SSA)

Peki CSA yerine SSA kullanınca ne oluyor?

Öncelikle tüm sorumluluk API Server’a yükleniyor ve tüm YAML API Server’a teslim ediliyor; last-applied annotation’ı olmuyor ve field management sistemi her şeyden sorumlu oluyor.

Ne yapıyor bu Field Management?

Diyor ki: “Her field’ın bir sahibi (owner) var ve sen o alanı sadece owner’ı isen değiştirebilirsin; aksi takdirde request’i reddederim. Owner’lığı da metadata’daki managedFields alanında tutarım.”

Örneğin: ArgoCD ile bir deployment yaptınız ve spec.containers[0].image değerine nginx:1.21 dediniz. managedFields‘e göre bunun owner’ı ArgoCD’dir. Ardından bir sebeple acil deployment yapmanız gerekti ve image’ı nginx:1.22 yapıp kubectl apply -f nginx.yaml --server-side ile deploy etmeye çalıştınız; otomatik olarak hata alırsınız. Elbette “Ben ne yaptığımı biliyorum, bunu yap” demek için süper gücünüzü (--force-conflicts argümanı) kullanabilir veya managedFields üzerinden o alanın owner’ını değiştirebilirsiniz. Fakat temel mantık bu: Kim neyin owner’ı tutmak ve buna göre karar vermek.

Karışık Kullanım Durumu

Peki ya karışık kullanırsanız? Yani toolları ayarladınız, Kyverno, ArgoCD, siz Server Side Apply ile çalışın, birbirinizi üzmeyin şeklinde ayarladınız, policylerinizi düzenlediniz; fakat sonrasında bir sebepten ötürü kubectl apply çalıştırma kararı aldınız. Doğal olarak objede last-applied yok, sadece managedFields var. Bu durumda önce bir uyarı mesajı görüyorsunuz:

resource deployments/nginx is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply

managedFields yerli yerinde duruyor, apply 3-way merge’i bir üye (last-applied) eksik şekilde yapıyor, last-applied‘ı ekliyor; fakat doğal olarak state biraz karışmış oluyor. Hem last-applied hem managedFields‘iniz oluyor; bu da deleted field ile ilgili sorun yaşayabilirsiniz demek.

Peki sonra ne oluyor? Burada işler CSA veya SSA’ya göre değişiklik gösteriyor. Validasyon kısmı zaten gerçekleşti, artık objenin oluşturulması gerekiyor. Önce SSA ile başlayalım, süreç çünkü daha basit.

SSA ile İstek Süreci

Request: /api/v1/namespaces/{namespace}/pods/{pod-name}?fieldManager=kubectl&fieldValidation=Strict&force=false şeklinde bir request, YAML’ın tamamıyla birlikte server’a Content-Type: application/apply-patch+yaml content type’ı ile gönderiliyor. Server pod oluşturmayı tamamlayıp kullanıcıya sonuç dönüyor.

CSA’da ise durum biraz daha karmaşıklaşıyor,

Öncelikle /api/v1/namespaces/{namespace}/pods/{pod-name} adresine request atılıp, bu objenin olup, olmadığı kontrol ediliyor. Varsa, lastApplied’a göre 3 way Merge yapılıp, PATCH request’i server’a gönderilecek. Obje yoksa, API Server 404 dönüyor, daha sonra kubectl bir request daha yapıp, namespace var mı diye kontrol ediyor. /api/v1/namespaces/{namespace} namespace mevcutsa, /api/v1/namespaces/{namespace}/pods?fieldManager=kubectl-client-side-apply&fieldValidation=Strict şeklinde request gönderip, objeyi oluşturuyor.

Pod oluşturma talebiniz, artık tüm engelleri kontrolleri aşarak Api server’a ulaştı, bundan sonra yolculuğuna orada devam edecek, orada ne oluyor diye incelemeye başlarsak süreç aşağı yukarı, Request > AuthN > APF > AuthZ > Admission Controllers > Storage. Bunlar üzerinden teker teker geçecek olursak;

Authentication (AuthN)

Tahmin edeceğiniz üzere önce request’i atan kişinin kimliğini belirleme süreci 3 ana farklı authentication ihtimali var

1. Client Certificates (mTLS)

kubectl ile komutunu çalıştırdığınızda API server öncelikle kendindeki TLS sertifikasını client’a dönüyor. Client, yani kubectl, current-context‘e yer alan kubeconfig dosyasındaki certificate-authority-data alanındaki sertifikayla doğruluyor, ve diyor ki tamam. Karşımdaki yabancı değil, ve doğrulamaya devam edebilirim, bu yüzden kubectl de kendinde yer alan, client-certificate-data ve client-key-data‘yi gönderiyor. API server gelen sertifika, kendi içinde yer alan CA bundle ile doğruluyor, bu sertifika bu CA tarafından imzalanmış mı diye. Bu CA node’da /etc/kubernetes/pki/ca.crt dizininde yer alan sertifika. Doğrulama tamamlandığında da, API server client sertifikasından Subject.CommonName‘dan Username’i Subject.Organization‘da grubu tespit ediyor.

2. Bearer Tokens

Bu aşamanın gerçekleşebilmesi için request’de Authorization: Bearer <token> olması gerekiyor. Burada da süreç yine dallara ayrılıyor tahmin edebileceğiniz gibi;

a. Statik Token

API Server’i ayağa kaldırırken --token-auth-file parametresiyle kaldırıp, uygun CSV dosyasını koyduysanız, ve token bu dosyada yer alıyorsa, tebrikler authentication’i tamamladınız demektir, csv’deki ilgili satırdan user ve grubu bulup, API server size ona göre davranacaktır.

b. ServiceAccount (in-cluster) token

Service Account’larda yer alan /var/run/secrets/kubernetes.io/serviceaccount/token dizinindeki JWT bazlı tokenlar (automountServiceAccountToken varsayılan olarak true burada false edilmediği senaryo var). Burada da süreç özetle, JWT’deki iss fieldi, ve --service-account-issuer ile karşılaştırılıp, --service-account-key-file‘daki sertifikayla validate edildikten sonra, JWT’deki field’larla hangi pod, namespace vs.. olduğu netleştiriliyor

c) OIDC (OpenID Connect) Burada onaylama süreci özetle şu şekilde işliyor, API Server’a diyoruz ki, request’i (yine JWT) --oidc-issuer-url adresinde belirtilen arkadaşla beraber doğrulayın.

3. Anonymous Access

Şu ana kadar hiç bir doğrulama metodundan geçemediysek, bi de server anonim olarak şansımızı deniyoruz --anonymous-auth=false olarak tanımlıysa (default değer true) kapı yüzümüze kapanıyor, ve API server’dan 401 response’u alıyoruz.

Not: APF (API Priority and Fairness)‘yi atlıyorum, çünkü yukarıda zaten bahsettim.

Authorization (AuthZ)

Kubernetes 6 farklı Authorization imkanı sunuyor bize. Bunlar: Node, RBAC, WebHook, ABAC, AlwaysAllow, AlwaysDeny. kube api-server’da --authorization-mode flag’i üzerinden tanım yapıp, değiştirebiliyoruz ve güzel yani birden fazla mod kullanabiliyorsunuz. Örneğin: --authorization-mode=Node,RBAC gibi. Doğal olarak bir authz talebi geldiği durumda, api-server bu talebi buradaki mode’lardan tamamına sırayla cevap almaya çalışıyor. Bunları teker teker kısaca tanımlayacak olursak;

Node

Bu özel bir authorization modudur. User authentication’undan ziyade node’larda yer alan kubelet’in service, pod, secret, configmap vs.. gibi resourcelarla işlem yapabilmesi için gerekli olan authorizatin metodudur. Authorization’un valid olabilmesi için öncelikle kubelet’in system:nodes grubuna ait system:node:{nodeName} formatında bir kullanıcıyla requesti yapması gerekmekte. Bunun için de tahmin edebileceğiniz üzere onaylanmış bir sertifikaya ihtiyacımız bulunuyor, bu da her node’da kubeadm join komutunu kullandığınızda başlayan bir süreç. kubeadm, bootstrap kısmını bitirdikten sonra kubelet’i çalıştırdığında kubelet node’a özel bir private key oluşturup, CSR’i api-server’a gönderiyor. Bu talep onaylandıktan sonra da certifika ve private key’i node’da /var/lib/kubelet/pki klasörüne kaydediyor, ve bundan sonraki iletişimlerde oradan kullanıyor.

Role Based Access Control (RBAC)

Günümüzde Kubernetes üzerinde kullanılan, user authorization için en yaygın metod diyebiliriz. Kullanıcılara, gruplara, service accountlara her obje için farklı yetki verebildiğiniz, role grupları oluşturup (ClusterRole, Role) kullanıcıları bu rollere atayabildiğiniz (ClusterRoleBinding, RoleBinding) muazzam bir yetkilendirme metodu. Güzel yani, ekstra bir operator kurmanıza gerek kalmıyor, API Server kendi bunu desteklediği için direkt kullanabiliyorsunuz.

Attribute Based Access Control (ABAC)

Biraz daha karmaşık ve legacy diyebileceğimiz bir yöntem. Kullanabilmeniz için --authorization-mode flag’ine ek olarak --authorization-policy-file= flag’i ile policy’nin yer aldığı yeri belirtmeniz gerekmekte. Örnek policy’lere https://kubernetes.io/docs/reference/access-authn-authz/abac/#examples buradan ulaşabilirsiniz.

WebHook

Tüm authorization sürecini external bir servise bıraktığınız yöntem. Bu yöntem sayesinde LDAP vs.. kullanabileceğiniz gibi kendi yetkilendirmenizi yapabilirsiniz. Buradan nasıl configure edebileceğinizi, örnek payload ve response görebilirsiniz.

AlwaysDeny

Tüm requestleri red ediyor. Kimse’nin yetkisi yok.

AlwaysAllow

Tüm requestleri onaylıyor. Ne olursan ol, gel dediğimiz durum. Tahmin edebileceğiniz üzere güvenlik riski yaratacak bir durum, dolayısıyla dikkatle yaklaşmak/kullanmak gerekiyor.

Süreç nasıl işliyor peki?

Birden fazla metodunuz var --authorization-mode=Node,RBAC. Öncelikle kullanıcı system:masters’in bir üyesi mi diye bakılıyor. Kullanıcı burada yetkili değilse, Node, değilse RBAC kontrol ediliyor, orada da yetkisi yoksa, otomatik olarak hata dönüyor fakat bu sıralama sırasında herhangi birinde Allow şeklinde dönüş olursa, diğerleriyle doğrulama yapmaya devam etmeden süreci bitiriyor.

system:masters nereden çıktı diye sorguluyorsanız bu hard-coded. Bunu “break-glass” senaryosu olarak düşünebilirsiniz. Biri yanlışlıkla ClusterRole’u sildi, tüm yetkiler kayboldu vs.. ne yapacaksınız? /etc/kubernetes/admin.conf kullanılamaz durumda, bu durumda /etc/kubernetes/super-admin.conf kullanıp, tüm güçlerinizi geri alacaksınız.

Admission Controllers

Kubernetes’de en sevdiğim kısım diyebiliriz. Kubernetes bu özelliklerle bize gelen request’i validate ve mutate imkanı tanıyor. Tabi sadece bize değil, kendine de bu imkanı tanıyor. Sistemin işlemesi için gerekli olan, fakat genelde default olarak tüm manifestlere eklenmeyen bir takım alanları bu aşamada kendi ekliyor. Örneğin serviceAccountName, eğer özel bir RBAC yapmadıysak, genelde bu alanı biz tanımlamayız, fakat kube-apiserver içerisindeki built-in ServiceAccount admission plugin’i bu tarz alanları otomatik olarak ekliyor.

Örn biz kubectl apply -f ile;

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - name: nginx
      image: nginx:1.27-alpine

Şeklinde bir obje oluşturuyoruz. Bu obje mutating policy’den geçerek;

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx:1.27-alpine
    imagePullPolicy: IfNotPresent
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-gvzrh
      readOnly: true
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  serviceAccount: default
  serviceAccountName: default
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-gvzrh
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

Gibi bir hal alıyor, burada ServiceAccount ile ilgili olan kısımlar (ServiceAccount,ServiceAccountName,volume etc..), priority, toleration vs.. kubernetes’in static mutating admission pluginiyle eklenen alanlar. Elbette bir kısmı condition’a bağlı olarak değiştirilebiliyor. Örn siz bir ServiceAccountName tanımladığınızda, Kubernetes mutating phase’inde bunu tanımlamıyor, sizin tanımladığınızla ilgileniyor. Bunun dışında burada bir sürü tanımlamadığımız alan var, bunların hepsi mutation policy’den mi geliyor? hayır, defaulting phase’inde diğer alanlar dolduruluyor. Peki default’ta neler ekleniyor, nasıl görebilirim diye merak ediyorsanız, aşağıdaki komutları sırasıyla çalıştırıp, deneyebilirsiniz

cat <<'EOF' >/tmp/admission-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - name: nginx
      image: nginx
EOF

kubectl apply -f /tmp/admission-pod.yaml --dry-run=client -o yaml >/tmp/client.yaml

kubectl apply -f /tmp/admission-pod.yaml --dry-run=server -o yaml >/tmp/server.yaml

diff -u /tmp/client.yaml /tmp/server.yaml

Mutating policy, kullanıcıya kolaylık sağlıyor, bunun üstüne kullanıcılar’a diyor ki; sen de istersen benim yaptıklarımın üzerine doğrulama veya geliştirme yapabilirsin. Burada ise devreye Mutating/Validating AdmissionWebhook devreye giriyor. Özetle Kubernetes’e “ben bir operator kullanıyorum, bu operator’un URL’i bu. Sana seçtiğim obje’ye request (get/list/watch hariç!) gelirse, bana da gönder, ben onun üzerine ekstra alan ekleyeceğim/sileceğim, veya bu obje’nin cluster’a deploy olup olmaması gerektiğine karar vereceğim.” diyebiliyoruz. Milyonlarca farklı kullanım amacı var, buradan örneğin Kyverno (örnek bir policy engine) için onlarca farklı örnek bulabilirsiniz. Örneğin objeler’e bir label’i zorunlu koşarsınız e2e-tested: true gibi, bu label’i sadece CI/CD ekleyebilecek ve operatorlerin bu label’a müdahalesini kısıtlarsınız. Bu sayede cluster’a deploy edilen objelerin e2e testlerden geçtiğini güvence altına alırsınız, ya da public registry’i kullanıldıysa manifestte engellersiniz vs… milyonlarca farklı kendi süreçlerinize uygun senaryo geliştirip, uygulayabilirsiniz.

Süreç ise aşağı yukarı şu şekilde gözüküyor;

API Server admission phases (simplified)
├── Mutating admission phase
│   ├── Built-in mutating admission plugins
│   ├── MutatingAdmissionPolicy
│   └── MutatingAdmissionWebhook (matching calls run serially)
│       ├── custom webhook A
│       ├── custom webhook B
│       └── custom webhook C
└── Validating admission phase
    ├── Built-in validating admission plugins
    ├── ValidatingAdmissionPolicy
    └── ValidatingAdmissionWebhook (matching calls run concurrently)
        ├── custom webhook A
        ├── custom webhook B
        └── custom webhook C

Burada önemli olan nokta bu bir async süreç değil. Eklediğiniz her webhook sync olduğu için request’inizin daha yavaş sonuçlanmasına sebep oluyor, ve request sırasıyla filtresine göre eşleşen, her policy tarafından sırasıyla denetleniyor. Bu eklediğiniz filtre, operatorunuzun hızı, reinvocationPolicy değeri, vs.. gibi bir sürü farklı değişkene bağlı. failurePolicy ayarına bağlı olarak da operator hata verdiği/çalışmadığı zaman ne yapılacağına karar verir. Örneğin Operator çalışmıyorsa, işlem diğer aşamalara geçmesin gibi.

Storage

Artık tüm doğrulama v.b gibi süreçleri tamamladığımıza göre veriyi etcd‘de saklayabiliriz. Burada da tabi bir takım field’lar ekleniyor. Örn: metadata.uid, metadata.creationTimestamp gibi. Daha sonrasında da obje’ye göre bir key atanıyor, bizim şu ana kadar örneklerimize göre bu /registry/pods/default/nginx olmalı. Çünkü default namespace’inde nginx pod objesini oluşturuyoruz, ETCD’de de saklanacak key bu oluyor. Burada önemli nokta ise --dry-run=server flag’i var mı yok mu. Bu flag varsa, süreç bu aşamaya kadar geliyor, fakat ETCD’ye yazılmadan geri dönüyor, yoksa da tebrikler, artık request’iniz database’e kayıt edildi!

Tabi, bu daha yolun yarısı diyebileceğimiz aşamadayız, obje’nin şu andan itibaren daha gidecek bir sürü süreci var, çünkü db’de yer alması demek onun otomatik olarak bir node’a assign edildiği, pod’un çalışmaya başladığı anlamına gelmiyor. Peki sırada ne var? Scheduling

Scheduling

Artık bi nefes alabiliriz çünkü request çoktan ETCD’ye kayıt edildi. Yani bundan sonrası pod’un çalışmasını beklemek (elbette image,taint vs.. gibi sorun yoksa). Bunun için ise sürecin nasıl işlediğine bakalım. Öncelikle API Server içerisinde bir process ETCD’de yer alan /registry/pods/ prefixini stream eder. Bu stream sırasında, yeni bir değer eklendiğinde bunu kendi cache’ine alır. Daha sonra kube-scheduler, API server’daki pods endpointini watch ettiği için, buradaki yeni eventleri filtreleyerek alır. Örn: kubectl get --raw '/api/v1/pods?watch=true' requesti ekstra parametrelerle çalışıyor diye düşünebilirsiniz. Burada farklı event tipleri göreceksiniz bunlar: ADDED, MODIFIED, DELETED, BOOKMARK, ERROR. Bizim şimdilik ilgilendiklerimiz ADDED, MODIFIED.

Sonuç olarak artık POD’unuz Scheduler’a da ulaştı. Scheduler kendi iç süreçlerinde farklı filtrelemeler, scorelar uygular (gelicez birazdan) bunu atlamanın büyülü bir noktası var. O da nodeName field’i. Eğer pod’unuzu oluştururken spec.nodeName fieldini tanımladıysanız, requestiniz Scheduler’in queue’sinden muaf demektir. Bu alan yoksa, 3 state’li bir queue’ye pod atanır. Buradaki statelerimiz backoffQ, unschedulablePods, activeQ. Örn: Pod’unuz 100 vcpu request ediyor, fakat şu anda clusterda onu sunabilecek bir node yok veya affinity’i karşılamıyor, bu durumda o pod unschedulablePods state’e alınıyor. Node ile ilgili bir değişiklik olursa (Örn bu state’i izleyip buna göre scaling yapan bir controlleriniz varsa) ya da node’a affinity’i karşılayacak bir label eklendiyse VE backoff süresi dolmadıysa buradan tekrar backoffQ‘ya alınır, aksi durumda ise activeQ‘ya. Pod’unuzun activeQ‘ya düştüğünü kabul edelim, burada sıralama priority ve queue timestampe göre yapılır. Yani Pod tanımında priority’si yüksek olanlar, scheduling için öncelik alır, aynı priority’e sahip podlar arasında ise queue’ye ilk alınan önceliklidir. (BackOff’a veya UnSchedulable’a düştükten sonra tekrar activeQ’ya geçerken bu değer sıfırlanır). Queue’de podunuz en zirvede, ve artık node atanmaya hazır olduğunu kabul ettiğimiz durumda ise işler biraz daha karmaşıklaşıyor. Kube-Scheduler elindeki Node, ve o Node’da kullanılan Pod, Pvc vs.. gibi bilgilerine göre seçim işlemlerine başlar. Bunun için de elbette önce filtreleme/skorlama yapması gerekiyor. Sıralama aşağı yukarı: TaintToleration > NodeAffinity > NodePorts > NodeResourcesFit > VolumeRestrictions > NodeVolumeLimits > PodTopologySpread > InterPodAffinity Yani özetle Pod’un taint/toleration’u, Affinity’si match oluyor mu, ilgili node’da kullanılan nodePort varsa, bu mevcut request’le çakışıyor mu, node’un allocatable resource’unu, node’da diğer yer alan podların resource req düşününce yeni pod oraya sığabilecek durumda mı, pod topology ve inter pod affinity açısından uyumsuz bir konu var mı diye filtreliyor. Tüm bunlara uyan tek bir node varsa, direkt olarak pod, o noda’a assign edilir, fakat birden fazla aday varsa, bu durumda skorlama işine girişilir. Sonuç olarak en yüksek skor’a sahip olan Node’a pod assign edilir. Api server’da /api/v1/namespaces/default/pods/nginx/binding endpoint’ine nodeName yazılır, podScheduled: true diye kabul edebiliriz. Bu otomatik olarak API Server’in MODIFIED state’inde bir event oluşturmasına sebep olur, ama zaten bu istenen bir durum.

Filtre kısmına biraz yakından bakacak olursak, öncelikle PreFilter kısmında podunuzun mevcut talepleri not edilir. 5 vcpu istiyorum, nvme disk istiyorum, affinity/antiAffinity’lerim bunlar diye. Dolayısıyla Node başına filtreleme işlemi yapılırken, bu bilgiler tekrar tekrar hesaplanmaz. Daha sonra “candidate” node’lar Filter aşamasına geçer, ve her bir node için, her filtre teker teker çalışır. Taint uyuyor mu, Node’a pod sığıyor mu vs.. diye. Burada iki tane önemli nokta var. Birincisi percentageOfNodesToScore konusu. Bu tamamen scheduling’i fine-tune etmeye yönelik bir durum. Özet olarak şu işe yarıyor. 100’den az node’unuz varsa, dert edecek fazla bir konu yok, fakat 100’den fazla node varsa, bu durumda her candidate node için, tüm bu filtreleri teker teker çalıştırmak (Tabii ki filtrelerden biri herhangi bir sebepten false dönüyorsa, diğer filtreler çalışmıyor o node için) node scheduling için gereksiz bir latency yaratabilecek bir durum. Çünkü, kube-scheduler’da maximum parallelism 16 (default, KubeSchedulerConfiguration ile değiştirilebiliyor). Yani Filtre aşamasında aynı anda maksimum 16 node filtre’ye uygun mu diye kontrol ediliyor. percentageOfNodesToScore den elde edilen formülle çıkan sonuç’a göre, Örn 900 node var, percentageOfNodesToScore’dan sonuç %50 çıktı, bu demek oluyor ki 450 tane sağlıklı, tüm filtreler tarafından onaylanan node varsa, kalan 450 node’u kontrol etmene gerek yok, direkt olarak bu 450 node’u score kısmına sokup, arasından hangisine schedule edeceğini belirleyebilirsin demektir. Kalan 450 node arasında daha yüksek score alabilecek bir node olabilir burada bi trade-off var. Amaç en hızlı şekilde node assign etmek, en uygun olan node’u bulmak değil burada. Node score konusu da tamamlandıktan sonra, pod, node’a assign edilmiş oluyor (Yukarıda örnek request yer alıyor)

Kubernetes retry storm’u önlemek adına exponential backoff kullanır. Jitter kullanmaz.

Pod Startup

Artık yavaş yavaş sona gelebiliriz. Pod’a nodeName assign edildi ve artık sıra ilgili node’daki kubelet‘de. Kubelet de kube-scheduler’a benzer şekilde API Server’i watch ediyor. Her node kendisine assign edilmiş Pod’ları, aşağı yukarı kubectl get --raw '/api/v1/pods?watch=true&fieldSelector=spec.nodeName%3D{nodeName}' request’ine denk gelen bir request ile buluyor. Burada önemli nokta kubelet’in scheduler’in Pod’u nasıl assign ettiğiyle ilgilenmemesi. spec.nodeName ilgili node’a eşit olduğu sürece Pod kubelet tarafından görülüyor. Dolayısıyla nodeName manifestte doğrudan tanımlandıysa scheduler bypass edilse bile kubelet Pod’u çalıştırmaya çalışıyor.

Kubelet bazı pre-requisitleri kontrol ediyor. Örn volume, secret, config map vs.. Volume’ler hazır olduktan sonra, kubelet CRI’dan pod sandbox’i oluşturmasını istiyor, CNI vs.. kullanarak networking oluşturuyor, image vs.. node’da yoksa indirildikten sonra Create/Start container işlemleriyle Pod çalıştırılıyor.

Daha sonra pod çalışır hale geldiğinde, API server aracılığıyla status.phase aşaması container statusuna göre update edilir.

Genel olarak, elimden geldiğince gerekli detaylara girip, çok fazla detaya boğmadan, önemli olduğunu düşündüğüm kısımları iletmeye çalıştım. Umarım, size bir şeyler katabilmiştir. Amacım, bu seriye devam edip, farklı konulara tekrar detaylı olarak girmek.