Bir Kubernetes Göçü Hikayesi

Selçuk Usta
10 min readAug 8, 2019

--

TL;DR — Tamamen müşterilerimiz tarafından kullanılan raporlama ve talep yönetimi uygulamamız, monolitik bir yapıda .NET Core çatısı üzerine kurulu olarak IIS üzerinde konumlanmaktaydı. Geçiş projemiz ile, uygulama içerisindeki modülleri fonksiyonel olarak gruplayarak RESTful servisler haline getirdik. Servislerimizi ve UI projemizi Dockerize ederek Kubernetes üzerinde konumlandırdık. Projenin mevcut monitoring, tracing ve logging altyapılarını da yeni sisteme adapte ederek olası sorunlar durumunda daha hızlı refleks gösterebilir bir ekip haline gelmeye yaklaştık.

Fotoğraf, Shripal Daphtary tarafından çekilmiş ve Unsplash üzerinde yayınlanmıştır.

En sonda söylenmesi gerekeni en başta söylemekte fayda var diye düşünüyorum. Bu geçiş hikayesinde kullanılan teknik ve teknolojiler bizim için bir reform noktası. Devam eden süreç içerisinde bazıları değişecek/yenilenecek ve bir noktada belki de kendini imha etmesi gerekecek. Sizin tespit ettiğiniz eksiklikler ya da hatalı olduğunu düşündüğünüz noktalar olursa yardıma aç ve açığız.

Yazı; kronolojik bir sıralamadan ziyade, hem süreç boyunca kendimize sorduğumuz hem de sosyal medyadan gelen sorulara yanıt verir şekilde bir kurgu ile oluşturuldu. Dolayısıyla interaktif bir yazı olma potansiyeline sahip. Gelecek olası sorularla birlikte zaman içerisinde güncellenecek. Lafı çok fazla uzatmadan, bol bol “neden” ve “nasıl” sorularına yanıt vermeye çalışacağım konuya gireyim o hâlde…

Teknoloji altyapı ve yatırımlarımızda neler var?

İş modelimizin detayına girmeden konuyu şöyle özetleyebilirim sanırım. Müşterilerimizden gelen operasyonel talep ve raporlamaları teknolojik olarak işleyen ve sunan bir ekibiz. Bu işi de, yazılımı paket olarak sunmak yerine yazılımı hizmet olarak sunma modeli ile gerçekleştiriyoruz. Bu hizmet modeli de tahmin ettiğiniz üzere web ve mobil sac ayakları üzerinde yaşamını devam ettiriyor. Bu ayakları güçlü bir şekilde tutabilmek, kesintisiz hizmet sağlayabilmek, yeni taleplere hızlı yanıt verebilmek ve uygun maliyetlerle ölçeklenebilir bir altyapı sunabilmek için Microsoft Azure kaynaklarıyla cloud üzerinde konumlanıyoruz.

Sanal makinelerden KeyVault servislerine, ServiceBus’tan Storage hizmetlerine kadar bir çok IaaS ve SaaS çözümlerini aktif olarak kullanıyoruz.

Tabi olduğumuz bazı regülasyonlar sebebiyle On-Prem’de kullanmak zorunda olduğumuz veritabanı, logging, monitoring gibi servislerimiz de mevcut. Bu iki dünya birbirinden çok ayrı değil ve aralarında sürekli bir iletişim mevcut ki yazının devamında bu detay bizi yeni bir çözüm bulmaya itiyor.

Proje geçiş fikri hangi noktada aklımıza yer etti?

Özette bahsettiğim web uygulamamızın, sıfır bir tasarım ile tekrar sahneye çıkması planı bir süredir gündemimizi meşgul ediyordu. Ekip olarak yaptığımız istişarelerde de; proje her ne kadar güncel teknolojilerle kurgulanmış bir yazılıma sahip olsa da devasa ve tekil yapısı sebebiyle canlı sisteme müdahale alanımızın dar olduğuna, yeni bir geliştirme talebini karşılarken etki analizlerinin fazlaca uzun sürdüğüne, yeni bir modülün kendi ihtiyacı olmasa dahi bazı bağımlılıkları üzerinde taşıması zorunluluğuna sahip olduğuna kanaat getirdik. Burada mimari olarak alınmış bazı yanlış kararların da etkin rol oynadığını söylemekte ve iğneyi kendimize batırmakta fayda var.

Sonuç olarak — uzun tartışmalar sonucunda — sadece tasarım giydirmektense projenin teknik altyapısını da yenilememiz gerektiğine karar verdik.

Sadece mimari hataları düzeltip monolitik olarak devam etmek varken, neden servis mimarisine geçildi?

Aşağıdaki soru bizim de bu kararı alırken üzerinde tartıştığımız en temel konuydu aslında.

Web ve mobil sac ayaklarından bahsetmiştik. Yeni tasarımla birlikte web uygulamamızda kullanmayı planladığımız rapor ve grafikleri, hali hazırda yayında olan ve henüz içini tam olarak dolduramadığımız mobil uygulamamıza da dahil etmeyi planladık. Eğer monolitik şekilde devam etseydik, bir süre sonra web uygulamasındaki kodların kopyalanıp yapıştırıldığı onlarca servisimiz olacak; “burası zaten çalışıyor, kodu silip servis çağrısı yapmak riskli” konfor balonunu doğuracak ve her biri en az iki defa yazılmış kod bloklarıyla dolu repository cehennemine merhaba demek zorunda kalacaktık. Bir diğer konu ise raporlar ve grafiklerdeki esneklik seviyemiz oldu. En basit haliyle durumu şöyle özetleyebilirim: Bir sayfadaki grafikler, olduğu gibi, servisten dönen cevap kadar ekrana — ve mobil uygulamaya — basılacak şekilde bir yapı tasarladık. UI tarafı da bu dinamizme ayak uyduracak şekilde planlandı. Monolitik yapıda devam edecek olsaydık, servise eklenen yeni bir özellik sebebiyle projenin tamamını yayına atmamız gerekecek, bu durum test ve deployment sürelerimizi uzatacaktı. Oysaki bir grafik servisinde değişen küçük bir yüzdelik dilim ya da başlık alanı için koskoca bir projeyi deploy etmek mantıklı mı dersiniz? Böylesi dinamik bir veri akış planına bu hantal ve sorumluluğu ağır süreçler çok aklımıza yatmadı açıkçası. Sonuç olarak bu emeği monolitiğe harcamak yerine servis yönelimli bir mimariye harcamayı tercih ettik.

Monolitik yapıdan servis yapısına geçerken yaşadığımız süreçler nelerdi?

Bu sürecin, her biri diğerinden farklı yöntemlere sahip bakış açıları mevcut. Henüz karşılaşmadıysanız bu adresteki yönelimleri incelemenizi öneririm. Biz, Shared Database yöntemini kullandık. Bazı servislerimiz birden fazla veritabanından aldığı veriyi kompozit hale getirip iletiyor. Doğal süreçte her servisin kendine ait bir veritabanı olması prensibi bize uymuyor. Tabii bunun için de farklı yöntemler mevcut. Her servise bir veritabanı, veritabanından veriyi alıp başka bir servise —API Composition — ileten bir başka servis de kullanılabilir. Ancak bu yaklaşım, zaman ve bakım maliyetimizi arttırabilir düşüncesiyle dışarıda kaldı.

Consumer olarak tasarladığımız ve hiçbir veri kaynağı ile direkt erişimi olmayan UI tarafında ise Server-side page fragment composition olarak ifade edilen yöntemi tercih ettik.

Kaynak

Buna göre, servisten gelen veriyi tüketip bir UI component’i haline getiren Partial View’ler ile sayfalarımızı ürettik. Böylece aynı grafiği ya da tabloyu farklı sayfalarda, ek bir efor gerektirmeden, kullanabiliyoruz.

En kritik noktalardan biri Circuit Breaker şablonunun UI tarafındaki implementasyonu idi. Henüz başlangıçta 22 adet olan servis sayısı muhtemelen önümüzdeki süreçlerde daha fazla sayıya çıkacak. Her servis çağrısının kendi içerisinde bir fallback ve retry senaryosu mevcut. Belirlediğimiz süre içerisinde belirlediğimiz sayı kadar denediğimizde istediğimiz HTTP cevabını alamazsak fallback senaryosu çalıştırdık ve bazı noktalarda component’i hiç göstermezken — client-side’da görmezden gelme durumu — bazı noktalarda ise kullanıcıyı bilgilendiren bir uyarı döndük.

Bir diğer önemli konu Logging konusu. Bu noktada daha önce defalarca deneyimlediğimiz ve hiç yarı yolda kalmadığımız ELK yöntemi ile kritik gördüğümüz her noktayı logladık. Loglarımızı JSON olarak logstash’e iletiyoruz. Bu kurguda aslına bakarsanız bir kaç önceki yazımda bahsettiğim yöntemi uygulamadık. Mevcutta kullandığımız log yapısını birebir geçirmeye karar verdik; en azından bu noktada bir konfor alanı elde edebilmek için. Bu değişiklik gelecek planlarımızda öncelik arz ediyor.

Uygulamalarımızı dockerize etme fikri, servis yapısına geçelim dediğimizde ön sıradan öncelikli listemize girmişti. build-env için mcr.microsoft.com/dotnet/core/sdk:2.2-alpine base image’ını; runtime aşaması içinse mcr.microsoft.com/dotnet/core/runtime-deps:2.2-alpine base image’ını kullandık. Böylece ortalama bir servisimizin image’ının boyutlarını 170~200 MB arasında tuttuk.

alpine image’ların SQL’e bağlantı esnasında yaşadığı kronik bir sorun mevcut: System.InvalidOperationException: Internal connection fatal error. Bu sorunla ilgili hatanın çözümü için de aşağıdaki kod bloğunu Dockerfile’larımıza ekledik:

Son olarak her bir servis projemize — ki hemen hemen hepsi bir veri kaynağından hizmet alıyor — Health Check ekledik. Bunun için ekstra bir kütüphane kullanmadık, .NET Core ile gelen özelliği kullanmayı tercih ettik. Health-check endpoint’lerini, yazının devamında da anlatacağım üzere servislerimizi Kubernetes’e deploy ederken livenessProbe olarak kullandık. Bu sayede, container process’i ayağa kalktığında veritabanına ulaşamıyor ise — ki HC 503 dönüyor — pod sağlıklı olarak kabul edilmiyor ve trafik yönlendirmesi yapılmıyor.

Kavimler Göçü — Kubernetes

Bu geçiş tam da başlıktaki — hatta yazı görselindeki — gibi servislerin büyük göçü oldu. Daha önceleri bazı servislerimiz Azure Virtual Machine üzerindeki IIS’lerde koşarken artık dockerize edilmiş olarak bir container orchestration aracı üzerinde koşacaklardı.

Azure konusundaki deneyimimizden ve aktif çözüm ortaklıklarımızdaki memnuniyetimizden ötürü KaaS olarak Azure Kubernetes Service’i tercih ettik. Ayrıca Aralık’18 ayında katıldığım bir Microsoft Open Hack etkinliğinde kendisiyle münasebetim olmuştu ve gayet de beğenmiştim.

Kurulum konusundaki basitliği ise takdire şayan:

Bu adımları tamamladıktan sonra 16GB RAM’li ve 4 CPU’lu 3 makine ile cluster yaklaşık 5 dakika içerisinde hazır hale geliyor. Tabi bu aşamadan öncesinde bizim yapmamız gereken bir hazırlık vardı.

On-Prem ile AKS network’ünü nasıl konuşturabiliriz?

Yukarıda bahsettiğim ve regülasyonlar sebebiyle iç network’te tutmak zorunda kaldığımız sistemlere ve yine Azure üzerinde ayrı network’lerde koşan IIS makinelere Kubernetes içerisindeki servislerimizden ulaşabilmeliydik.

Bu durumu sağlayabilmek için Kubernetes kurulumundan önce bir VNET oluşturduk ve diğer VNET’ler arasında peering gerçekleştirdik. Sonrasında yeni oluşturduğumuz bu VNET’i Kubernetes kurulumuna parametre olarak geçtik: --vnet-subnet-id “[CLUSTER_SUBNET_ID]”

Ingress Network

22 servis ve 1 UI projesini Kubernetes’e deploy etmek için gerekli deployment ve service tanımlamalarının yer aldığı manifest dosyalarını hazırlamaya koyulduk. 1domain ve 1 sub-domain üzerinden hizmet vermemiz gerekli: foo.com ve api.foo.com

Tabi her servisi Load Balancer olarak expose etmeyi aklımızdan dahi geçirmedik. Bunun yerine Kubernetes’in Ingress objesini kullandık. İlk etapta Nginx Ingress Controller’ı tercih ettik.nginx tecrübesine sahip olduğum için kullanımı oldukça kolay diye düşünüyorum. Yapı itibariyle de oldukça basit bir kaynak. Ancak tek bir IP üzerinden 2 farklı host’a hizmet etme özelliği mevcut değil (eğer sub-domain değilse). Bizim durumumuz subdomain olduğu için kurtarıyordu. Bu sefer de ikinci bir engelle karşılaştık:

UI domain’ine gelen istekler bir rewrite kuralından geçmeden direkt olarak service’e yönlendirilecek. Ancak API tarafından gelen istekler bir rewrite kuralına ihtiyaç duyuyor. Örneğin api.foo.com/company , CompanyService’in /path’ine trafiği yönlendirmesi gerekirken api.foo.com/address , AddressService’in /path’ine yönlendirme yapmalı. Nginx Ingress’te bu işi ingress.kubernetes.io/rewrite-target: / annotation ‘ı ile yönetebiliyoruz.

UI tarafında kural şu şekilde olması gerekirken;

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/rewrite-target: /
name: ingress-ui
namespace: default
spec:
rules:
- host: foo.com
http:
paths:
- path: /
backend:
serviceName: ui-service
servicePort: 80

Servis tarafında şu şekilde olması gerekiyor:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: custom-nginx
ingress.kubernetes.io/rewrite-target: /$2
name: ingress-dev
namespace: default
spec:
rules:
- host: api.foo.com
http:
paths:
- path: /company(/|$)(.*)
backend:
serviceName: company-service
servicePort: 80
- path: /address(/|$)(.*)
backend:
serviceName: address-service
servicePort: 80

Nginx Ingress Controller’da birden fazla rewrite-target desteği bulunmuyor. Bunun yerine Helm üzerinden yeni bir ingress oluşturmanız gerekiyor. Bu da yeni bir Public IP demek.

Istio

Eş zamanlı olarak Istio’yu denemeye başladık. Ancak bu taraftaki deneyim, Nginx kadar güçlü değildi ve AR-GE tarafı daha çok zaman aldı. Istio’yu aşağıdaki tanımlama ile kurduk. Böylece Grafana ile metrikleri izlerken, Kiali ile de service mesh’i gözlemleme fırsatına sahip olduk:

Kurulum sonrasında şu hata başımızı çok ağrıttı:

kubectl logs istio-sidecar-injector-12345b6-789cd -n istio-system Error: failed to create injection webhook could not watch /etc/istio/inject/config: no space left on device

Cluster node’larındaki diskleri defalarca kontrol ettik ancak herhangi bir sorun yakalayamadık. Sonrasında bu sorunu Azure Kubernetes Service parantezinde araştırdık.fs.inotify.max_user_watches değeri AKS node’larında varsayılan olarak 8192 olarak ayarlaymış. Bunu yükselten bir DaemonSet deploy ettik ve sorun çözüldü.

SSL Termination

UI ve API domain’i HTTPS üzerinden hizmet veriyor. Bu işi IIS üzerinde yönetmek her zaman yaptığımız işlerden biriydi. IIS’e siteyi deploy et, 443 binding’i ayarla, bir sertifika seç, hepsi bu kadar!

Ancak iş, bir load balancer arkasından hizmet etmeye gelince ve SSL’i load balancer ile çözüp, backend servislerine HTTP üzerinden erişme noktasına gelince; değişiyor. Neyseki hem Nginx Ingress hem de Istio üzerinde bu işi gerçekleştirmek çok zor değil. SSL sertifikasını kubectl create secret ile oluşturmak ve gerekli konfigürasyona bu secret adını tanımlamak yeterli oluyor.

Ancak UI tarafında atladığımız bir konu, biraz canımızı yaktı diyebilirim. Uygulamamız Identity Server ile authentication işlerini yürütüyor. Bu tarafta da client’ın redirect_uri ‘sini https://foo.com/signin-oidc olarak tanımlamıştık. IIS üzerindeyken herhangi bir forwarder bulunmuyor, dolayısıyla host:port ikilisi olduğu gibi Identity Server’a iletiliyor.

Ancak yeni durumda; SSL’i çözen Istio, trafiği http://ui gibi bir backend’e yönlendiriyor ve bu backend üzerinden Identity Server’a gidiliyor. Tabii forwarded_proto http, forwarded_port ise 80 olarak işlem görüyor ve Identity Server tarafından, “tanımlamayan client” muamelesi görüyoruz.

Bu sorunu aşmak için UI tarafına — ASP.NET Core MVC projesi — ForwardedHeaders middleware’ını implemente etmemiz gerekti. Böylece load balancer’dan gelen proto ve port bilgileri direkt olarak backend tarafından kullanılabilir duruma gelecekti. Ayrıca Istio tarafındaki UI manifest’ine yandaki satırları ekledik.

Monitoring, Tracing, Logging nasıl işliyor?

Logging mekanizması yukarıda da değindiğim üzere ELK ile yönetiliyor. Bir nlog.config dosyası Kubernetes içerisinde secret olarak tutuyoruz ve manifest dosyaları içerisinden volume ile container’a bind ediyoruz. Böylece config üzerinde bir değişiklik olduğunda tüm container’lara bu değişikliği hızlıca dağıtabiliyoruz.

Service Mesh’i monitor etmek için, Istio ile gelen Kiali’yi kullanıyoruz. Böylece API’ler ve UI arasındaki trafiği, response time’ları, error rate’leri izleyebiliyoruz.

Ayrıca cpu, memory, disk gibi metrikleri de yine İstio ile yüklenen Grafana üzerinden takip edebiliyoruz.

Son durumda, container’lar arasındaki sağlıklı bağlantı, hata ve izleme log’ları, metrikler elimizde oluyor ve sorunlara daha hızlı yanıt verebiliyoruz.

Deployment süreçlerimiz nasıl kurgulandı?

Kodlarımızı Azure DevOps üzerinde depoluyoruz. Build ve release süreçlerimiz de yine buradan yönetmeyi tercih ettik. Release flow yöntemi ile geliştirmelerimizi yapıyoruz. Akışı şöyle canlandırabiliriz:

Bir master branch’imiz var ve push’a kapalı durumda. Yalnızca pull request alıyor. Ekipteki geliştirici arkadaşlarımız, yeni bir task geldiğinde bu task için feature/JIRA-1234 isimli bir branch oluşturuyor. Geliştirmesini tamamlıyor ve branch’ini pull request olarak yolluyor. Code-review süreçlerinden sonra branch master’a merge oluyor.

Deploy vakti geldiğinde master’ın son haline bir tag atıyoruz: foo-ui-v1.0.0 Azure DevOps tarafındaki pipeline yalnızca tag’ler ile çalışıyor. Tag’i parçalıyor ve yine Azure üzerindeki Container Registry’e push edilebilecek hale getiriyor: foo-ui:1.0.0 ve foo-ui:latest Sonrasında Dockerfile’ı build ediyor ve bu versiyonlarla push işlemini gerçekleştiriyor. Son adımda Kubernetes manifest dosyası apply ediliyor. Varsayılan olarak RollingUpdate stratejisi ile hareket ediyoruz. Böylece bir önceki versiyon’u taşıyan deployment yayına devam ederken yeni versiyon HC’e giriyor. Başarılı olursa trafiği kendi üstüne alıyor ve önceki deployment kaldırılıyor. Böylece kesintisiz hizmet’i sağlamış oluyoruz.

Sonuç

Bizim için yoğun ve yorucu bir süreç oldu, ancak ekibin büyük özverisiyle, hafif takvimi sarkıtmış olsak da — yayına çıkış sürecimizi tamamladık. Çok şey öğrendik ve öğrenmeye devam ediyoruz. Yeni süreçten de mutlu gibiyiz. Süreçlerimize katkınız olursa ayrıca mutlu oluruz.

Buraya kadar sabırla okuyan olursa teşekkürlerimi kabul etsin, umuyorum az da olsa bu hikayeyi farklı renk bir kalemle yazmaya niyetli meslektaşlarıma yardımcı olabilmişimdir.

Geriye kalan tüm sorular ve dertleşmeler için buradan, mail yoluyla ya da sosyal medya üzerinden tartışmaya açığım. Bir sonraki yazıya dek HC’leriniz 503 görmesin :)

--

--

Selçuk Usta

Engineering Manager (at) Hepsiburada. Former trainer & consultant. Tweets are mostly about tech and coding. https://superpeer.com/selcukusta