Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Kubernetes Kullanarak Konteynerleştirme – Pratik İnceleme

Merhaba,

Bu içeriğimizde, önceki Kubernetes ile ilgili yayınlamış olduğum Kubernetes Nedir? Temel Kavramları Nelerdir? ve Kubernetes’in Mimarisi Nasıldır? başlıklı makalelerin ardından artık Kubernetes’i pratiksel olarak inceleyecek ve Kubernetes ile konteynerleştirmenin detaylarına temas edeceğiz.

Kubernetes ile bir uygulamayı konteynerleştirmek için önce mimarisel sürece dair felsefeyi tam olarak kavramak gerektiği kanaatindeyim. Bunun için aşağıdaki 4 aşamalı sürece ilk etapta teoride hakim olunması elzemdir diyebiliriz:

Aşama Ne yapıyoruz? Mantığı
1.   Uygulamanın paketlenmesi
      (container image)
Uygulama kodunu bir image haline getiriyoruz! Uygulama açısından taşınabilirlik sağlıyoruz.
2.   Pod’un tanımlanması
      (deployment/pod yaml)
Kubernetes’e bu container’ı nasıl çalıştıracağını söylüyoruz. Orkestrasyon sağlıyoruz.
3.   Service’in tanımlanması
      (ingress)
Dış dünya veya diğer Pod’ların bu container’a erişimini sağlıyoruz. Ağ yönetimi sağlıyoruz.
4.   Ölçekleme ve yönetim
      (HPA, ConfigMap, Secret)
Uygulama yükü, yapılandırma, güvenlik gibi yönleri ele alıyoruz. Dayanıklılık ve sürdürülebilirlik sağlıyoruz.

Bu dört adım, Kubernetes ile konteynerleştirme sürecinin teorik omurgasını sağlamaktadır.

Buradan anlıyoruz ki, Kubernetes uygulamaları direkt çalıştırmamakta, yalnızca container image’lerini çalıştırmaktadır. Yani uygulamanın önce bir Docker Image haline getirilmesi gerekmektedir.

Kubernetes’in Container’ı Nasıl Çalıştıracağını Tanımlama (Manifest)

Kubernetes’e, oluşturulan image ile ne yapılacağını söyleyebilmek için manifest adı verilen YAML tabanlı bir bildirimsel (declarative) tanım dosyası oluşturmamız gerekmektedir. Bu dosyanın yapısı aşağıdaki gibidir;

apiVersion: apps/v1        # Nesnenin API sürümü
kind: Deployment           # Nesnenin türü
metadata:
  name: myapp              # İsim, etiketler vb.
spec:                      # Asıl yapılandırma (özellikler)
  replicas: 3
  template:
    .
    .
    .

Bu şablon üzerinden her nesne türü için bir tanımlamada bulunabiliriz. Bu nesne türleri nelerdir? diye sorarsanız eğer hemen aşağıda detaylı bir şekilde açıklayalım;

  • Pod : Tek bir uygulama konteynerini tanımlamaktadır.
  • Deployment : Uygulamayı belirli sayıda Pod olarak çalıştırmaktadır.
  • Service : Pod’lara sabit IP/DNS ile erişim sağlamaktadır.
  • Ingress : Http/Https trafiğini yönlendirmektedir.
  • ConfigMap : Uygulama yapılandırmalarını tutmaktadır.
  • Secret : Parola, API key vs. gibi gizli bilgileri saklamaktadır.

Bu manifest dosyası sayesinde bizler Kubernetes’e -şunu yap- minvalinde bir talimat vermiş olmuyoruz! -Ben sistemin bu durumda olmasını istiyorum- şeklinde bir dille talebimizi iletmiş oluyoruz. Kubernetes’de bu duruma ulaşmak için sorumluluğu üstlenerek gereken her şeyi kendi yapmaya çalışıyor. Olay bundan ibaret 🙂

Dikkat ederseniz, bu manifest dosyasının Kubernetes açısından hangi nesneye karşılık geldiğini (yani ne olduğunu) kind ile, bu yapılandırmaya göre Kubernetes’in nasıl davranış sergileyeceğini spec ile ve bu yapılandırmanın nereye (hangi uygulamaya) ait olduğunu ise metadata ile belirtiyoruz.

Aşağıda Deployment nesnesine dair bir manifest dosyasının tam bir örneğini göreceksiniz:

deployment.yaml;

apiVersion: apps/v1
kind: Deployment
metadata:
  name: exampleapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: exampleapp
  template:
    metadata:
      labels:
        app: exampleapp
    spec:
      containers:
        - name: exampleapp
          image: mcr.microsoft.com/dotnet/samples:aspnetapp
          ports:
            - containerPort: 8080

Buradaki alanların ne olduğunu tek tek izah etmemiz gerekirse eğer;

  • apiVersion
    Kubernetes API’sinin hangi sürümünün kullanılacağını belirtmektedir.
  • kind
    Hangi tür Kubernetes nesnesinin tanımlandığı belirtilmektedir. Deployment cevabı ile Pod’ların yönetiminin yapılandırıldığı anlaşılmaktadır.
  • metadata
    Nesneye ait kimlik bilgilerini tutmaktadır.
  • spec
    Specification’ın kısaltmasıdır. Kubernetes’e bu nesnenin nasıl davranacağının bilgisini vermektedir.
  • replicas
    Uygulamanın kaç instance’ının olacağını belirtmektedir.
  • selector
    Bu Deployment‘ın hangi Pod’ları yöneteceğini belirtmektedir. Buradan anlaşılıyor ki, bu manifest dosyası -app- değeri ‘exampleApp’ olan tüm Pod’ları hedeflemektedir.
  • template
    Bu Deployment‘ın oluşturacağı Pod’ların şablonudur. Her Pod bu template’ten kopyalanarak oluşturulur. Böylece bir değişiklik olduğu taktirde tüm Pod’lar yeniden yaratılacaktır.
  • template → metadata
    Pod’un kendi metadata bilgilerini içerir. İçerisindeki -app- değeri selector’dakiyle eşleşmek mecburiyetindedir. Böylece Deployment ve Pod arasındaki bağ, bu label eşleşmesiyle kurulacaktır.
  • template → spec
    Pod’un içeriğini yani hangi container’ların çalışacağını belirtmektedir. Container name değeri, loglama süreçlerinde ilgili container’a dair referans olarak değerlendirilecektir. Bu kubectl logs mypod -c exampleApp talimatı eşliğinde kontrol edilebilir.

Örneği incelersek eğer, içerisinde öğrenme/deneme amaçlı hazır Asp.NET Core örnek uygulaması barındıran mcr.microsoft.com/dotnet/samples:aspnetapp image’ı üzerinden Kubernetes ile 3 kopya (replica) olacak şekilde bir Deployment denetiminin sağlandığını görmekteyiz. Tabi bu manifest’te Service/Ingress tanımı olmadığı için bu uygulamaya dışarıdan ulaşılamayacağını bilmekte fayda vardır. İçeriğimizin sonraki satırlarında bir Service nesnesi üzerinden bu uygulamayı nasıl dışarıya erişilebilir hale getirebileceğimizi de vurgulayacağız.

Şimdilik olayı daha da net kavrayabilmek için yapılan işlemi beşeri dil mantığında özetleyerek devam edelim…

Merhaba Kubernetes! Ben -exampleapp- adında bir uygulama çalıştırmak istiyorum. Bu uygulamadan 3 tane Pod çalışsın. Her Pod’un içinde mcr.microsoft.com/dotnet/samples:aspnetapp image’ından bir container olsun. Uygulama 8080 portunda çalışsın ve bu Pod’lar app: exampleapp etiketiyle işaretlensin. Ben de o etikete göre onları yönetebileyim.

İşte Kubernetes’e, bu manifest ile bu minvalde bir izahta bulunmuş vaziyetteyiz. Artık gerisi Kubernetes’tedir. Tabi bu durum manifest dosyasını çalıştırdığımız sürece geçerli olacaktır. Bunun için kubectl apply -f deployment.yaml talimatıyla bu manifest’teki tanımlar sisteme sürülebilir ve uygulama için gerekli olan tüm Kubernetes servislerinin, deployment’ların ve gerekli olan tüm kaynakların Kubernetes tarafından oluşturulması sağlanabilir.Kubernetes Kullanarak Konteynerleştirme - Pratik İncelemeBu talimat verildiği taktirde, esasında arkaplanda şu şekilde bir cereyan söz konusu olmaktadır;

  1. kubectl komutu, aktif context’e bakarak hangi cluster’a gerekli deploy’un yapılacağını öğrenir. Bunu bizlerde öğrenmek için kubectl config current-context veya kubectl config get-contexts talimatlarından istifade edebiliriz.
  2. Manifest dosyası Kubernetes API Server’a gider. API Server, Kubernetes sistemindeki iletişim kapısı ve merkezi komuta noktasıdır. Haliyle, bundan kaynaklı Kubernetes’te kubectl komutları, manifest dosyaları vs. API Server üzerinden sistemle konuşmaktadırlar. Kısaca API Server, Kubernetes’te cluster’a yapılan tüm istekleri yöneten bir bileşendir.
  3. Cluster içindeki Control Plane Node‘u ilgili yaml dosyasını alır.
  4. Bu Node içerisindeki Scheduler yapısı devreye girerek ilgili Pod’ların hangi Node’da ayağa kaldırılması gerektiğine karar verir.
    Tabi bu karar sürecinde aşağıdaki gibi uygun kaynaklar ve şartlar göz önünde tutulur:

    • Node’un CPU ve RAM kapasitesi yeterli mi?
    • Pod bir nodeSelector veya affinity ile belirli bir Node’u özellikle istiyor mu?
    • Node özel bir iş için ayrılmış mı, yoksa Pod buna toleranslı mı?
    • Node’lar arasında load balancing sağlanıyor mu?
    • vs.

    Bu ve bunlara benzer kurallar dikkate alınarak ilgili Pod uygun Node’da ayağa kaldırılır.

  5. Nihai olarak seçilen Node üzerindeki Kubelet servisi, Pod’u oluşturur ve içindeki container’ı başlatır. Artık o Pod, o Node üzerinde çalışmaktadır.
Service Manifest’i Oluşturma ve Uygulamayı Dışarıdan Erişilebilir Hale Getirme

Yukarıdaki manifest dosyasını çalıştırdığımız halde tarayıcı üzerinden localhost:8080 adresine istekte bulunulursa eğer uygulamaya dışarıdan erişim yapılamadığı görülecektir. Bu duruma önceki satırlarda temas etmiştik. Bunun nedeni, ilgili uygulamayı dışarıdan erişime açacak olan bir Service nesnesinin tanımlanmamış olmasıdır. Tabi burada dikkat edilmesi gereken nokta şudur ki; bir uygulamayı dışarı açabilmek için Service her daim tek başına bir belirleyici değildir! Esasında Service‘in tipi belirleyicidir. Varsayılan ClusterIP yalnızca cluster içinden erişim sağlayacak bir tiptir. Halbuki komple dışarıya açabilmek için NodePort, LoadBalancer ya da Ingress(+Service) gerekmektedir. Şimdi gelin bunların mukayesesini gerçekleştirelim;

Kaynak/Tip Dışarıdan erişim Ne zaman kullanılır?
Service: ClusterIP (default) ❌(yalnızca cluster içi) Cluster içi servisler arası haberleşme süreçlerinde tercih edilebilir. Pod’lara stabil olarak sanal IP/DNS verir ve L4 yük dengelemesi sağlar. Dış dünyaya sızmaz, sade ve güvenlidir.
Service: NodePort ✅ <NodeIP>:<NodePort> üzerinden Cloud Load Balancer yoksa ve hızlıca dışarıdan erişim sağlanması gerekiyorsa tercih edilebilir. Uygulama için 30000-32767 arasında bir port açar. Production ortamları için kalıcı çözüm değildir!
Service: LoadBalancer ✅ Cloud LB IP/DNS ile Cloud’dan veya doğrudan dış IP/DNS alsın isteniyorsa kullanılabilir. Http dışındaki (TCP/UDP, MQTT vs.) gibi protokollerde dışa açılacaktır.
Ingress(+Service) ✅ Host/path, TLS, L7 yönlendirme Http/Https bazlı yönlendirme istendiğinde, tek domain altında birden çok servisin söz konusu olduğu senaryolarda(app.com, app.com/api, admin.app.com vs.) tercih edilebilir.
ExternalName / Headless ❌ (DNS yönlendirme / keşif) Cluster içerisindeki uygulamaların dış bir servise takma ad ile yönlendirilebilmesi durumlarında (DNS yönlendirme) tercih edilebilir.

Aşağıda NodePort türünden örnek bir Service manifest dosyası bulunmaktadır;

service.yaml;

apiVersion: v1
kind: Service
metadata:
  name: exampleapp-service
spec:
  selector:
    app: exampleapp
  ports:
    - name: http
      port: 80                # Service port (cluster içi)
      targetPort: 8080        # Pod içindeki containerPort
      nodePort: 30000         # Dışarıdan erişilecek Node portu (30000-32767 aralığı)
  type: NodePort

Bu manifest’i çalıştırabilmek için kubectl apply -f service.yaml talimatının verilmesi yeterli olacaktır.Kubernetes Kullanarak Konteynerleştirme - Pratik İncelemeBurada dikkat ederseniz, nodePort alanına 30000 portu bildirilerek uygulamaya localhost:30000 adresi üzerinden erişilebilir bir yapılandırmada bulunulmaktadır.Kubernetes Kullanarak Konteynerleştirme - Pratik İncelemeİşte bu kadar basit 🙂

Load Balancing Davranışı

Yukarıda yaptığımız Deployment ve Service kurulumları neticesinde load balancing davranışı otomatik devrede olacaktır. Şöyle ki; Service nesnesi (NodePort),
(app: exampleapp) selector değeriyle eşleşen 3 Pod’u tek endpoint altında toplayacak ve kube-proxy L4 seviyesinde trafiği bu Pod’lara otomatik olarak dağıtacaktır.

Tabi burada her istekte kesin başka bir Pod’a yönlendirme yapılacağının garantisi verilmemektedir! Şöyle ki; Kubernetes Service‘in yaptığı load balancing TCP düzeyinde gerçekleşecektir. Yani, aynı TCP bağlantısı üzerinde kalan çoklu istekler aynı Pod’a gidecek, yeni bir bağlantı açıldığı taktirde de kube-proxy o bağlantıyı başka bir Pod’a yönlendirebilecektir (ihtimaldir) Bu yüzden çok sayıda kısa ömürlü bağlantıda ‘kabaca’ dengeli dağılım görülebilmekte, amma velakin her isteğin farklı Pod’a yönlendirilmesi garanti edilememektedir.

İstendiği taktirde aşağıdaki gibi aynı kullanıcıyı hep aynı Pod’a yapıştırmak da (sticky) mümkündür:

service.yaml;

apiVersion: v1
kind: Service
metadata:
  name: exampleapp-service
spec:
  selector:
    app: exampleapp
  ports:
    - name: http
      port: 80                # Service port (cluster içi)
      targetPort: 8080        # Pod içindeki containerPort
      nodePort: 30000         # Dışarıdan erişilecek Node portu (30000-32767 aralığı)
  type: NodePort
  sessionAffinity: ClientIP   # istemci IP’sine göre yapışkanlık
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800

Tabi bu şekilde bir manifest dosyasının içerisini günceller ve tekrardan kubectl apply -f service.yaml talimatını işlersek ilgili Service nesnesi yerinde (in-place) güncellenecek ve önceki haline nazaran yeni gelen alanlar (sessionAffinity, sessionAffinityConfig vs.) anında instance’a patch edilecektir.Kubernetes Kullanarak Konteynerleştirme - Pratik İncelemeBöylece Pod’lar restart edilmeksizin, sadece Service kuralları güncellenecek ve yeni ayarlar, o andan itibaren tüm yeni bağlantılara hemen uygulanıyor olacaktır.

Nihai olarak;

İçerik sürecinde Kubernetes teknolojisiyle konteyner’lerin nasıl çalıştırılabileceğine, bunun için manifest dosyalarının gerekliliğine, yapılandırma süreçlerine ve ayağa kaldırılmış konteyner’lerin dışarıdan erişilebilir olması için Service nesnesi ile gerekli yapılandırma detaylarına bir nebze temas etmiş ve olayı pratiksel olarak temelde deneyimlemiş bulunuyoruz. Bundan sonraki içeriklerimizde Kubernetes ile ilgili tecrübelerimizi derinleştirecek daha farklı teknikleri ele alacak ve parça bütün ilişkisiyle tam teferruatlı zihnimizde oturtana kadar incelemeye devam ediyor olacağız.

O halde,

Sonraki yazılarımda görüşmek üzere…
İlgilenenlerin faydalanması dileğiyle…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki GitHub adresinden erişebilirsiniz.
https://github.com/gncyyldz/Kubernetes_Example

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir