Bildiğimiz gibi Microsoft, bu yıl Barcelona KubeCon‘da bir çok yeniliklerini duyurdu. Bence bunlardan önemli bir tanesi ise SMI(Service Mesh Interface) idi.
İncelediğim kadarıyla SMI‘ın tanımı için kısaca, tıpkı AMQP‘de olduğu gibi “interoperability” konusunu service mesh’ler arasında sağlayabilmek diyebilirim. Özünde service mesh’ler için Kubernetes üzerinde standart bir interface sunmaktadır. Böylece service mesh için provider-lock durumuna düşmeden, istediğimiz teknolojiyi kullanabilmemiz için bir olanak, abstraction sağlamaktadır.
SMI‘ın duyurulmasının ardından, uzun süredir incelemek için aklımda olan Linkerd service mesh’i inceleyebilmek için bir fırsat buldum ve hakkında bir şeyler yazmak istedim.
İlk olarak service mesh nedir ve bize neler sağlıyor, kısaca değinmek istiyorum.
Bildiğimiz gibi bir çok organizasyon günümüz teknolojisine ve marketine ayak uydurabilmek ve o marketten büyük bir pay alabilmek için, microservice mimarisine ayak uydurmaya, adapte olmaya çalışıyor.
Bu adaptasyon sürecinde ise asıl önemli olan nokta, birbirlerinden decoupled hale getirilmiş olan servislerin, birbirleriyle nasıl hızlı, resilient ve secure bir şekilde iletişim içerisinde olacaklarıdır. Elbette load-balancing, traffic management ve health monitoring de cabası. Bu gereksinimlerin bir çoğunu, zaten farklı tool’lar vasıtasıyla implemente ediyoruz. Örneğin resiliency konusunu, gerek API Gateway’ler, gerekse de Polly gibi bazı framework’ler vasıtasıyla uygulamaların içerisinde implemente ediyoruz.
Peki, service mesh ne sunuyor?
Service mesh ise, service-to-service communication’ını manage ederek, “resiliency“, “scalability“, “security” ve “monitoring” gibi bazı kavramları farklı çözümler ile ele almamız yerine, bu tarz network işlemlerini kodumuzdan decoupled etmemize olanak sağlıyor. Ayrıca bu gereksinimleri bize kendisi tek bir elden sunuyor.
Linkerd, CNCF tarafından desteklenen kubernetes için open source bir service mesh’dir.
Çalışma şekline baktığımızda ise, transparent bir sidecar proxy instance’ı olarak her bir servis’in yanında konumlandığını görebiliriz. A servis’inden B servisini direkt olarak çağırmak yerine, B servis’inin local proxy’sini çağırarak, buradaki “service-to-service” communication complexitiy’lerini bizim için encapsulate etmektedir.
Genel hatlarıyla Linkerd’nin bazı key özellikleri:
Mimarisi hakkında daha detaylı bilgiye, buradan erişebilirsiniz.
Linkerd‘nin kurulum işlemine geçmeden, öncelikle bir kubernetes cluster’ına ihtiyacımız var. Ben bu noktada, Azure‘un managed Kubernetes Service‘inden yararlanacağım. Eğer Azure Kubernetes Service‘e sahip değilseniz, buradan oluşturabilirsiniz.
İlk önce aşağıdaki komut satırı ile Azure‘a login olalım.
az login
NOT: Bu işlemler için Azure CLI‘ın kurulu olması gerekmektedir. Değilse, buradan erişebilirsiniz.
Ardından aşağıdaki komut satırı ile de, cluster’a erişebilmek için gerekli credential’ları alalım.
az aks get-credential --resource-group={YOUR_AKS_RESOURCE_GROUP} --name {YOUR_AKS_NAME}
Şimdi linkerd‘nin kurulumu için hazırız.
Kurulumu gerçekleştirebilmek için, buradaki ilk 3 adımı tamamlamamız gerekiyor. Adımları tamamladıktan sonra her şeyin yolunda gittiğinden emin olabilmek için ise, aşağıdaki komut satırını kullanalım.
linkerd check
Ardından aşağıdaki gibi bir sonuç görüyor olmalıyız.
Artık mesh’lemeye hazırız. Örnek gerçekleştirebilmek adına, aşağıdaki gibi 3 adet içerisinde swagger barındıran basit API’lar geliştirdim.
Yukarıda görmüş olduğumuz product response’unu alabilmemiz için, “Product.Gateway.API” bizim için hem “Product.API” a hem de “Price.API” a request gönderecek. İlgili response’ları aggregate ettikten sonra ise bize full product response’unu dönüyor olacak.
API‘lara buradan erişebilirsiniz.
İlk olarak “Product.Gateway.API” ın “ProductsController” ını inceleyelim.
using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; namespace ProductGateway.API.Controllers { [Route("api/products")] [ApiController] public class ProductsController : ControllerBase { private readonly IHttpClientFactory _clientFactory; private readonly IConfiguration _configuration; public ProductsController(IHttpClientFactory clientFactory, IConfiguration configuration) { _clientFactory = clientFactory; _configuration = configuration; } [HttpGet("{productId}")] public async Task<ActionResult<GetProductResponse>> Get([FromRoute]int productId) { var productDetail = GetProductDetailAsync(productId); var productPrice = GetProductPriceAsync(productId); await Task.WhenAll(productDetail, productPrice); return Ok(new GetProductResponse { ProductId = productDetail.Result.ProductId, Name = productDetail.Result.Name, Description = productDetail.Result.Description, Price = productPrice.Result.Price }); } private async Task<GetProductDetailResponse> GetProductDetailAsync(int productId) { GetProductDetailResponse productDetailResponse = null; HttpClient client = _clientFactory.CreateClient(); string productApiBaseUrl = _configuration.GetValue<string>("Product_API_Host"); HttpResponseMessage response = await client.GetAsync(requestUri: $"{productApiBaseUrl}/api/products/{productId}"); if (response.IsSuccessStatusCode) { productDetailResponse = JsonConvert.DeserializeObject<GetProductDetailResponse>(await response.Content.ReadAsStringAsync()); } return productDetailResponse; } private async Task<GetPriceResponse> GetProductPriceAsync(int productId) { GetPriceResponse productPriceResponse = null; HttpClient client = _clientFactory.CreateClient(); string priceApiBaseUrl = _configuration.GetValue<string>("Price_API_Host"); HttpResponseMessage response = await client.GetAsync(requestUri: $"{priceApiBaseUrl}/api/prices?productId={productId}"); if (response.IsSuccessStatusCode) { productPriceResponse = JsonConvert.DeserializeObject<GetPriceResponse>(await response.Content.ReadAsStringAsync()); } return productPriceResponse; } } public class GetProductResponse { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } public double Price { get; set; } } public class GetProductDetailResponse { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } } public class GetPriceResponse { public int ProductId { get; set; } public double Price { get; set; } } }
“Get” method’u içerisinde basit olarak, product detay’larını ve fiyat bilgilerini alabilmek için, ilgili API‘lara request gönderiyoruz. API base URL‘lerini ise, configuration üzerinden okuyoruz. Kubernetes üzerine API‘ları deloy edeceğimiz zaman, bu API URL‘lerini environment variable olarak set ediyoruz olacağız.
API‘ları dockerize edebilmek için ise, buradaki ilgili Dockerfile‘ları kullanalım.
“Product.Gateway.API” için örnek Dockerfile:
#Build Stage FROM microsoft/dotnet:2.2-sdk AS build-env WORKDIR /workdir COPY ./src/ProductGateway.API ./src/ProductGateway.API/ RUN dotnet restore ./src/ProductGateway.API/ProductGateway.API.csproj RUN dotnet publish ./src/ProductGateway.API/ProductGateway.API.csproj -c Release -o /publish FROM microsoft/dotnet:2.2-aspnetcore-runtime COPY --from=build-env /publish /publish WORKDIR /publish EXPOSE 5000 ENTRYPOINT ["dotnet", "ProductGateway.API.dll"]
Ben container registry olarak Azure Container Registry servis’ini kullanacağım.
Aşağıdaki komut satırı ile image’leri oluşturalım ve container registry’e push edelim.
docker build -f ./*.Dockerfile . -t {YOUR_CONTAINER_REGISTRY}/*-api:dev az acr login --name {YOUR_CONTAINER_REGISTRY_NAME} docker push {IMAGE_NAME_WITH_TAG}
Kubernetes üzerine deployment işlemi için ise, buradaki yaml file’larını kullanacağız.
“Product.Gateway.API” için örnek deployment ve service file’ı:
--- apiVersion: v1 kind: Namespace metadata: name: linkerd-test --- apiVersion: apps/v1 kind: Deployment metadata: name: product-gateway-api-deploy namespace: linkerd-test spec: replicas: 1 selector: matchLabels: app: product-gateway-api template: metadata: labels: app: product-gateway-api spec: containers: - name: product-gateway-api image: ggplayground.azurecr.io/product-gateway-api:dev imagePullPolicy: Always ports: - containerPort: 5000 name: http resources: requests: cpu: 100m memory: 100Mi env: - name: Product_API_Host value: http://product-api-svc.linkerd-test:9090 - name: Price_API_Host value: http://price-api-svc.linkerd-test:8080 --- apiVersion: v1 kind: Service metadata: name: product-gateway-api-svc namespace: linkerd-test spec: type: LoadBalancer selector: app: product-gateway-api ports: - port: 80 targetPort: http
API‘ları, “linkerd-test” isimli namespace altına deploy edeceğiz. “Product.Gateway.API” içerisinde ilgili API URL‘lerini environment variable olarak set edeceğimizi söylemiştik. Dikkat edersek “env” section’ı altında, hem “Product” hem de “Price” API‘ının service adreslerini set ettik.
Şimdi 3 API‘ın da deployment işlemlerini, ilgili yaml file’ları ile gerçekleştirelim.
kubectl apply -f price-api-deploy.yaml kubectl apply -f product-api-deploy.yaml kubectl apply -f product-gateway-api-deploy.yaml
Deployment’ların başarıyla gerçekleşip gerçekleşmediğini aşağıdaki gibi kontrol edelim.
kubectl get deploy -n linkerd-test
Şimdilik her şey yolunda görünüyor.
Birde API‘ları test edelim. Bunun için “Product.Gateway.API” ının dışarıya expose olduğu service adresini almamız gerekiyor.
kubectl get svc -n linkerd-test
NOT: External IP adresi alması bir kaç dakika sürebilir.
Test işlemini gerçekleştirebilmek için, “Product.API” ve “Price.API” içerisine id’si “1” olan dummy bir product eklemiştim.
Test edebilmek için, “Product.Gateway.API” a aşağıdaki gibi bir request gönderelim ve sonucuna bir bakalım.
http://{YOUR_EXTERNAL_IP}/api/products/1
Harika, API‘lar da çalışıyor.
Şimdi mesh’leyebilmek için tek yapmamız gereken, linkerd‘yi API‘lara inject etmek. Bu işlemi gerçekleştirebilmek için ise, aşağıdaki gibi linkerd‘nin CLI‘ından faydalanacağız.
kubectl get -n linkerd-test deploy -o yaml | linkerd inject - | kubectl apply -f -
Yukarıdaki komut satırı ile, “linkerd-test” namespace’i altındaki uygulamalarımıza, linkerd’yi inject ediyoruz.
Şimdi linkerd‘nin dashboard’u üzerinden, neler oluyor bir bakalım.
Dashboard’a erişebilmek için:
linkerd dashboard &
Overview kısmında bizi yukarıdaki gibi bir ekranla karşılıyor. Burada deployment ve pod’ları görebiliyoruz. Ayrıca hangi service’leri mesh’lediğimizi de görebilmek mümkün.
En sevdiğim kısımlardan birisi ise metric‘ler. Her service’e özel “Success Rate“, “RPS” ve “Latency” gibi bilgilerini görebiliyoruz.
Şimdi ilk giriş noktası olan “Product.Gateway.API” için, “Deployments” bölümü altındaki “product-gateway-api-deploy” deployment’ına tıklayalım ve detaylarına bir bakalım.
Ardından biraz metric görebilmek için, aşağıdaki gibi “Product.Gateway.API” a biraz request gönderelim. Ben bu işlem için ApacheBench kullanacağım.
ab -n 1000 http://{YOUR_EXTERNAL_IP}/api/products/1
En sevdiğim diğer bir kısım ise, automatic service dependency map‘i ve live traffic bilgilerini bize sunması. Dependency map’ine bakarsak, “product-gateway-api” ın hem “price-api” hem de “product-api” ile bağlantılı olduğunu görebiliriz.
Ayrıca “LIVE CALLS” sekmesinden, o anda gerçekleşen call’ların sample’larını da görebiliriz.
Peki, değinmek istediğim güzel bir konu daha var. Route-based runtime metric‘leri ve retry‘lar.
Linkerd içerisinde önemli konulardan bir tanesi de service profile’larıdır. Service profile’ları, linkerd‘ye ilgili service hakkında ek bilgiler sunan custom bir kubernetes resource’udur.
Service profile’larını tanımlayarak, linkerd‘nin bize her bir service için “route-based runtime metrics” verebilmesini sağlayabiliriz. Ayrıca “retries” ve “timeouts” gibi feature’larını da etkinleştirebiliriz.
Service profile’larını tanımlayabilmenin “Swagger“, “Protobuf“, “Auto-Creation” ve “Template” gibi bir kaç farkı yöntemi mevcut. Ben API‘ları geliştirirken swagger implemente ettiğim için, service profile tanımlayabilmek için swagger yöntemini kullanacağım.
API‘ların ilgili swagger file’larına, buradan erişebilirsiniz.
Profile tanımlayabilmek için, aşağıdaki komut satırını kullanacağız.
linkerd -n linkerd-test profile --open-api ./price-api-swagger.json price-api-svc | kubectl -n linkerd-test apply -f - linkerd -n linkerd-test profile --open-api ./product-api-swagger.json product-api-svc | kubectl -n linkerd-test apply -f - linkerd -n linkerd-test profile --open-api ./product-gateway-api-swagger.json product-gateway-api-svc | kubectl -n linkerd-test apply -f -
Evet, service profile’ları oluşmuş durumda. Şimdi route-based metric’leri görebileceğiz.
Tekrardan “Product.Gateway.API” a biraz request gönderelim. Bu sefer deployments ekranından “price-api-deploy” un “ROUTE METRICS” tab’ına bir bakalım.
Gördüğümüz gibi, service profile ile route-based metric’leri görebiliyoruz. Şimdi birde retry’ları nasıl tanımlayabiliriz, ona bir bakalım.
Örneğin, “Price.API” ın “Get” endpoint’ine gelen request’lerin bazılarının faile’a düştüğünü varsayalım ve otomatik olarak retry özelliğini etkinleştirmek istiyoruz.
Bunu gerçekleştirebilmek için, aşağıdaki gibi oluşturmuş olduğumuz service profile’ını edit’leyerek, ilgili route için “isRetryable” variable’ını eklememiz gerekmektedir.
kubectl -n linkerd-test edit sp/price-api-svc.linkerd-test.svc.cluster.local
Hepsi bu kadar.
Dilerseniz customize edebilmek için, “Retry Budget” mekanizmasını da kullanabilirsiniz. Detaylı bilgiye, buradan ulaşabilirsiniz. API kodlarına herhangi bir müdehale etmeden, “retry” ve “timeouts” gibi fonksiyonalite’leri ekleyebilmek, güzel bir capability.
Son olarak değinmek istediğim bir diğer konu ise, Grafana desteği. Live metric’lerin yanında, geçmiş metricleri’de Prometheus ve Grafana desteği ile visualize edebilmek mümkündür.
Service mesh, microservice mimarileri için önemli bir infrastructure layer’dır. Network’ü abstract ederek, distributed mimarilerin challenge’larını (reliability, security, monitoring, etc…), kodumuz içerisindeki complexity’i arttırmadan handle etmemize yardımcı olmaktadır. Linkerd2 diğer service mesh’lere göre henüz tüm özelliklere sahip olmasa da, özellikle intelligent load balancing’i (low-latency) ile kullanabileceğimiz iyi bir service mesh seçeneğidir.
Demo app: https://github.com/GokGokalp/service-mesh-linkerd-sample
https://linkerd.io/2/getting-started/
https://www.zdnet.com/article/what-is-a-service-mesh-and-why-would-it-matter-so-much-now/
{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…
{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…
{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…
{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…
{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…
{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…
View Comments
Hocam şöyle bir durumda nasıl bir yol izlememiz lazım. price ve product servislerinin authorize ile erişmemiz gerektiğini düşünürsek authorize işlemini ProductGateway üzerinde mi yapmamız lazım yoksa güvenlik açısından product ve price servislerinde ayrı ayrı yapmak mı daha doğru?
Selam, benim görüşüm ilgili API'lar ayrı ayrı authorization işlemini kendisi gerçekleştirmeli. Örneğin, Price API içerisinde kullanıcının fiyat bilgisini alabilme claim'i olabilir, fakat update etme claim'i olmayabilir. O yüzden ilgili API'ın kendisinin gerçekleştirmesi daha doğru olacaktır.
nacizane detayli bilgi meraklisina docker routing mesh loadbalancer algoritmasi ...
Bu arada yalin bir anlatim olmus emegine saglik :)