Playing with Service Mesh – Linkerd ve Azure Kubernetes Service

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.

Service Mesh, huh?

İ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.

Peki, Linkerd?

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:

  • Intelligent Load Balancing (HTTP/HTTP2, gRPC): Request’leri en hızlı endpoint’e gönderebilmek için EWMA (Exponentially Weighted Moving Average) adında bir algoritma kullanmaktadır
  • Automatic Retries and Timeouts
  • Automatic mTLS
  • Powerfull Telemetry and Monitoring features: Observability için önemli özelliklerinden bir tanesi)
  • Dashboard and Grafana

Mimarisi hakkında daha detaylı bilgiye, buradan erişebilirsiniz.

Ön Hazırlık

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.

Let’s Play!

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 “PriceAPI‘ı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.

Service Profiles

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.

Route-based Metrics

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.

Retries

Ö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.

Sonuç

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

Kaynaklar

https://linkerd.io/2/getting-started/
https://www.zdnet.com/article/what-is-a-service-mesh-and-why-would-it-matter-so-much-now/

Gökhan Gökalp

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 :)

Recent Posts

Event-Driven Architecture’larda Conditional Claim-Check Pattern’ı ile Event Boyut Sınırlarının Üstesinden Gelmek

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

2 ay ago

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{: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.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago