Service Mesh with Linkerd in a .NET Microservice Architecture

DotNet Full Stack Dev
6 min readJul 24, 2024

--

A service mesh is a dedicated infrastructure layer for handling service-to-service communication, often in a microservices architecture. It provides several capabilities such as service discovery, load balancing, failure recovery, metrics, and monitoring. Linkerd is a popular service mesh for Kubernetes, which can be integrated into a .NET microservices application.

In this example, we’ll use Linkerd to enhance the communication between Product and Order services in a Kubernetes environment.

Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.

Why Service Mesh?

  • Service Discovery: Automatically discovers services and their instances.
  • Load Balancing: Distributes requests evenly across instances.
  • Failure Recovery: Retries failed requests and performs circuit breaking.
  • Metrics and Monitoring: Provides detailed observability into service communication.
  • Security: Encrypts service-to-service communication and provides identity-based access control.

What is Linkerd?

  • Lightweight Service Mesh: Designed to be simple and lightweight.
  • Kubernetes Native: Integrates seamlessly with Kubernetes.
  • Observability: Provides out-of-the-box metrics, monitoring, and tracing.

Example Overview

We’ll deploy Product and Order services in a Kubernetes cluster and use Linkerd to manage their communication.

Prerequisites

  • .NET Core 6.0
  • Kubernetes Cluster (Minikube, AKS, etc.)
  • Linkerd CLI
  • Docker (for containerizing applications)

Steps

  1. Set Up Kubernetes Cluster
  2. Install Linkerd
  3. Create Docker Images for Product and Order Services
  4. Deploy Services to Kubernetes
  5. Inject Linkerd Sidecar Proxy
  6. Access Metrics and Monitoring

Step 1: Set Up Kubernetes Cluster

Set up a local Kubernetes cluster using Minikube or any other Kubernetes provider.

minikube start

Step 2: Install Linkerd

Install the Linkerd CLI and check the cluster’s readiness for Linkerd.

# Install Linkerd CLI
curl -sL https://run.linkerd.io/install | sh

# Add Linkerd CLI to your path
export PATH=$PATH:$HOME/.linkerd2/bin

# Verify installation
linkerd version

# Validate cluster
linkerd check --pre

# Install Linkerd
linkerd install | kubectl apply -f -

Step 3: Create Docker Images for Product and Order Services

ProductService Dockerfile:

# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["ProductService/ProductService.csproj", "ProductService/"]
RUN dotnet restore "ProductService/ProductService.csproj"
COPY . .
WORKDIR "/src/ProductService"
RUN dotnet build "ProductService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]

OrderService Dockerfile:

# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["OrderService/OrderService.csproj", "OrderService/"]
RUN dotnet restore "OrderService/OrderService.csproj"
COPY . .
WORKDIR "/src/OrderService"
RUN dotnet build "OrderService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "OrderService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderService.dll"]

Build and push the Docker images:

docker build -t your-docker-repo/productservice -f ProductService/Dockerfile .
docker build -t your-docker-repo/orderservice -f OrderService/Dockerfile .

docker push your-docker-repo/productservice
docker push your-docker-repo/orderservice

Step 4: Deploy Services to Kubernetes

Create Kubernetes deployment and service manifests for Product and Order services.

ProductService Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
name: productservice
spec:
replicas: 2
selector:
matchLabels:
app: productservice
template:
metadata:
labels:
app: productservice
spec:
containers:
- name: productservice
image: your-docker-repo/productservice
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: productservice
spec:
selector:
app: productservice
ports:
- protocol: TCP
port: 80
targetPort: 80

OrderService Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
name: orderservice
spec:
replicas: 2
selector:
matchLabels:
app: orderservice
template:
metadata:
labels:
app: orderservice
spec:
containers:
- name: orderservice
image: your-docker-repo/orderservice
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: orderservice
spec:
selector:
app: orderservice
ports:
- protocol: TCP
port: 80
targetPort: 80

Deploy to Kubernetes:

kubectl apply -f productservice.yaml
kubectl apply -f orderservice.yaml

Step 6: Access Metrics and Monitoring

Linkerd provides a dashboard for metrics and monitoring. Access it using:

linkerd dashboard

Example Overview

  • Product Service: Handles product-related operations.
  • Order Service: Handles order-related operations and interacts with the Product Service.
  • Linkerd: Manages service-to-service communication, providing observability and security.

Use Case: Adding Inventory Service to Existing Order and Product Services with Linkerd

In this use case, we will extend the microservices architecture by adding an InventoryService to the existing OrderService and ProductService. We'll illustrate how Linkerd simplifies this integration and enhances the overall system's capabilities.

Initial Setup (Without Linkerd)

Architecture Overview

  1. OrderService: Handles order creation and management.
  2. ProductService: Manages product information.
  3. InventoryService: Keeps track of product stock levels.

Challenges Without Linkerd

  1. Service Discovery: Manual configuration needed for each service to discover and communicate with other services.
  2. Load Balancing: Custom logic required to distribute requests across multiple instances.
  3. Resilience: Manual implementation of retry logic and circuit breakers.
  4. Observability: Limited and manually configured logging and monitoring.
  5. Security: Complex and error-prone setup to secure inter-service communication.

Enhanced Setup (With Linkerd)

Architecture Overview

  1. OrderService: Handles order creation and management.
  2. ProductService: Manages product information.
  3. InventoryService: Keeps track of product stock levels.
  4. Linkerd: Automatically manages service discovery, load balancing, resilience, observability, and security.

Benefits with Linkerd

  1. Automatic Service Discovery: Services are automatically discovered and can communicate seamlessly.
  2. Built-In Load Balancing: Efficiently distributes requests across service instances.
  3. Enhanced Resilience: Automatic retries and circuit breakers improve reliability.
  4. Comprehensive Observability: Detailed metrics and tracing for all services.
  5. Improved Security: Mutual TLS (mTLS) secures communication between services.

Implementation Details

Step 1: Define Services

OrderService: Handles order-related operations. ProductService: Manages product data. InventoryService: Tracks inventory levels.

Step 2: Configure Services

Without Linkerd:

  • Manually configure service endpoints and discovery mechanisms.
  • Implement custom load balancing, retries, and security.

With Linkerd:

  • Services register themselves with Linkerd for automatic discovery.
  • Linkerd handles load balancing, retries, and security.

Code Implementation

Let’s walk through the implementation of the InventoryService and how it integrates with OrderService and ProductService.

InventoryService

InventoryController.cs:

[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private readonly IInventoryService _inventoryService;

public InventoryController(IInventoryService inventoryService)
{
_inventoryService = inventoryService;
}

[HttpGet("{productId}")]
public async Task<IActionResult> GetStockLevel(Guid productId)
{
var stockLevel = await _inventoryService.GetStockLevelAsync(productId);
return Ok(stockLevel);
}

[HttpPost]
public async Task<IActionResult> UpdateStockLevel([FromBody] StockUpdateModel model)
{
await _inventoryService.UpdateStockLevelAsync(model.ProductId, model.Quantity);
return Ok();
}
}

IInventoryService.cs:

public interface IInventoryService
{
Task<int> GetStockLevelAsync(Guid productId);
Task UpdateStockLevelAsync(Guid productId, int quantity);
}

InventoryService.cs:

public class InventoryService : IInventoryService
{
private readonly Dictionary<Guid, int> _stock = new();

public Task<int> GetStockLevelAsync(Guid productId)
{
if (_stock.TryGetValue(productId, out var stockLevel))
{
return Task.FromResult(stockLevel);
}

return Task.FromResult(0);
}

public Task UpdateStockLevelAsync(Guid productId, int quantity)
{
_stock[productId] = quantity;
return Task.CompletedTask;
}
}

Step 3: Update OrderService to Communicate with InventoryService

OrderService.cs:

public class OrderService : IOrderService
{
private readonly HttpClient _httpClient;
private readonly IProductService _productService;

public OrderService(HttpClient httpClient, IProductService productService)
{
_httpClient = httpClient;
_productService = productService;
}

public async Task<Order> CreateOrderAsync(OrderCreateModel model)
{
var product = await _productService.GetProductByIdAsync(model.ProductId);
var stockLevelResponse = await _httpClient.GetAsync($"http://inventory/api/inventory/{model.ProductId}");

if (!stockLevelResponse.IsSuccessStatusCode)
{
throw new Exception("Unable to retrieve stock level.");
}

var stockLevel = await stockLevelResponse.Content.ReadAsAsync<int>();

if (stockLevel < model.Quantity)
{
throw new Exception("Insufficient stock.");
}

// Create and save the order
var order = new Order
{
Id = Guid.NewGuid(),
ProductId = model.ProductId,
Quantity = model.Quantity,
TotalPrice = product.Price * model.Quantity
};

// Assuming SaveOrderAsync saves the order in a database
await SaveOrderAsync(order);

// Update stock level
await _httpClient.PostAsJsonAsync("http://inventory/api/inventory", new { ProductId = model.ProductId, Quantity = stockLevel - model.Quantity });

return order;
}
}

Step 4: Configure Linkerd for Automatic Management

  • Deploy Linkerd: Follow Linkerd’s documentation to deploy it to your Kubernetes cluster.
  • Annotate Services: Annotate your services for Linkerd injection.
apiVersion: apps/v1
kind: Deployment
metadata:
name: orderservice
namespace: default
annotations:
linkerd.io/inject: enabled
spec:
replicas: 3
template:
metadata:
labels:
app: orderservice
spec:
containers:
- name: orderservice
image: orderservice:latest
ports:
- containerPort: 80

Repeat this annotation for ProductService and InventoryService.

Benefits of Linkerd in This Use Case

  1. Automatic Service Discovery: No need to manually configure service endpoints. Linkerd automatically discovers and routes requests between services.
  2. Built-In Load Balancing: Requests to InventoryService are automatically load-balanced, ensuring efficient use of resources.
  3. Enhanced Resilience: Linkerd provides retries and circuit breaking for inter-service calls, increasing system reliability.
  4. Comprehensive Observability: Linkerd offers detailed metrics and tracing, helping you monitor service interactions and performance.
  5. Improved Security: With mTLS, Linkerd secures communication between OrderService, ProductService, and InventoryService.

Conclusion

Integrating Linkerd into your microservices architecture, including OrderService, ProductService, and the newly added InventoryService, significantly enhances the system's capabilities. Linkerd simplifies service discovery, load balancing, resilience, observability, and security, allowing you to focus on developing business logic rather than managing infrastructure complexities. This setup ensures a robust, scalable, and secure microservices architecture, ready to meet modern application demands.

You may also like : https://medium.com/@siva.veeravarapu/zookeeper-in-net-microservice-architecture-120d2d7ed562

--

--

DotNet Full Stack Dev
DotNet Full Stack Dev

Written by DotNet Full Stack Dev

Join me to master .NET Full Stack Development & boost your skills by 1% daily with insights, examples, and techniques! https://dotnet-fullstack-dev.blogspot.com

No responses yet