Service Mesh with Linkerd in a .NET Microservice Architecture
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
- Set Up Kubernetes Cluster
- Install Linkerd
- Create Docker Images for Product and Order Services
- Deploy Services to Kubernetes
- Inject Linkerd Sidecar Proxy
- 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
- OrderService: Handles order creation and management.
- ProductService: Manages product information.
- InventoryService: Keeps track of product stock levels.
Challenges Without Linkerd
- Service Discovery: Manual configuration needed for each service to discover and communicate with other services.
- Load Balancing: Custom logic required to distribute requests across multiple instances.
- Resilience: Manual implementation of retry logic and circuit breakers.
- Observability: Limited and manually configured logging and monitoring.
- Security: Complex and error-prone setup to secure inter-service communication.
Enhanced Setup (With Linkerd)
Architecture Overview
- OrderService: Handles order creation and management.
- ProductService: Manages product information.
- InventoryService: Keeps track of product stock levels.
- Linkerd: Automatically manages service discovery, load balancing, resilience, observability, and security.
Benefits with Linkerd
- Automatic Service Discovery: Services are automatically discovered and can communicate seamlessly.
- Built-In Load Balancing: Efficiently distributes requests across service instances.
- Enhanced Resilience: Automatic retries and circuit breakers improve reliability.
- Comprehensive Observability: Detailed metrics and tracing for all services.
- 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
- Automatic Service Discovery: No need to manually configure service endpoints. Linkerd automatically discovers and routes requests between services.
- Built-In Load Balancing: Requests to
InventoryService
are automatically load-balanced, ensuring efficient use of resources. - Enhanced Resilience: Linkerd provides retries and circuit breaking for inter-service calls, increasing system reliability.
- Comprehensive Observability: Linkerd offers detailed metrics and tracing, helping you monitor service interactions and performance.
- Improved Security: With mTLS, Linkerd secures communication between
OrderService
,ProductService
, andInventoryService
.
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