Skip to content

Service Mesh

Service Mesh is an infrastructure layer used to manage service-to-service communication in microservice architecture. In this section, we will examine Istio and Spring Boot integration in detail.

Quick Decision

SituationUse MeshWatch Out
Many services and shared traffic policyYesOperations team must be ready
Need mTLS, traffic split, retry standardYesBad policies have wide impact
Few services, simple trafficNoGateway/library may be enough
Debugging is already hardBe carefulSidecars add visibility and complexity

Production Checklist

  • Problem: Which repeated communication problem does the mesh centralize?
  • Solution: Are mTLS, authorization policy, retry, timeout, traffic split, and observability standards clear?
  • Trade-off: You gain shared policy and visibility; you add sidecar resource, latency, and operational cost.
  • Failure mode: Misconfigured policies, sidecar crashes, control-plane outages, and retry amplification should be handled.
  • Measurement: Track sidecar CPU/memory, request latency, policy deny rate, mTLS errors, and control-plane health.
  • Security/cost: Mesh simplifies security but wildcard policies are risky; every sidecar creates capacity cost.

Istio Service Mesh

Sidecar Proxy Pattern

Envoy Proxy Configuration:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-config
data:
  envoy.yaml: |
    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address:
          protocol: TCP
          address: 127.0.0.1
          port_value: 9901
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address:
            protocol: TCP
            address: 0.0.0.0
            port_value: 10000
        filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              access_log:
              - name: envoy.access_loggers.stdout
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
              http_filters:
              - name: envoy.filters.http.router
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: service_cluster
      clusters:
      - name: service_cluster
        connect_timeout: 30s
        type: LOGICAL_DNS
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        load_assignment:
          cluster_name: service_cluster
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: order-service
                    port_value: 8080

Istio Injection Configuration:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: microservices
  labels:
    istio-injection: enabled
    
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: microservices
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
      version: v1
  template:
    metadata:
      labels:
        app: order-service
        version: v1
      annotations:
        sidecar.istio.io/inject: "true"
        sidecar.istio.io/proxyCPU: "100m"
        sidecar.istio.io/proxyMemory: "128Mi"
    spec:
      containers:
      - name: order-service
        image: order-service:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "istio"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Traffic Management

VirtualService Configuration:

yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service-vs
  namespace: microservices
spec:
  hosts:
  - order-service
  - order-service.microservices.svc.cluster.local
  gateways:
  - order-gateway
  - mesh
  http:
  # Canary deployment - 10% traffic to v2
  - match:
    - headers:
        canary:
          exact: "true"
    route:
    - destination:
        host: order-service
        subset: v2
  # A/B Testing based on user segment
  - match:
    - headers:
        user-segment:
          exact: "premium"
    route:
    - destination:
        host: order-service
        subset: v2
      weight: 50
    - destination:
        host: order-service
        subset: v1
      weight: 50
  # Default routing with gradual rollout
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 90
    - destination:
        host: order-service
        subset: v2
      weight: 10
    fault:
      delay:
        percentage:
          value: 0.1
        fixedDelay: 5s
      abort:
        percentage:
          value: 0.01
        httpStatus: 500
    timeout: 30s
    retries:
      attempts: 3
      perTryTimeout: 10s
      retryOn: gateway-error,connect-failure,refused-stream

DestinationRule Configuration:

yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service-dr
  namespace: microservices
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
        connectTimeout: 30s
        tcpKeepalive:
          time: 7200s
          interval: 75s
      http:
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
        maxRequestsPerConnection: 10
        maxRetries: 3
        consecutiveGatewayErrors: 5
        h2UpgradePolicy: UPGRADE
    circuitBreaker:
      consecutiveGatewayErrors: 5
      consecutiveServerErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
      minHealthPercent: 50
    outlierDetection:
      consecutive5xxErrors: 5
      consecutiveGatewayErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
      minHealthPercent: 50
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      connectionPool:
        tcp:
          maxConnections: 50
  - name: v2
    labels:
      version: v2
    trafficPolicy:
      connectionPool:
        tcp:
          maxConnections: 100

Security Features

PeerAuthentication Configuration:

yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: microservices
spec:
  mtls:
    mode: STRICT

---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: order-service
  namespace: microservices
spec:
  selector:
    matchLabels:
      app: order-service
  mtls:
    mode: STRICT
  portLevelMtls:
    8080:
      mode: STRICT

AuthorizationPolicy Configuration:

yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: order-service-authz
  namespace: microservices
spec:
  selector:
    matchLabels:
      app: order-service
  rules:
  # Allow authenticated users to access orders
  - from:
    - source:
        principals: ["cluster.local/ns/microservices/sa/order-service"]
    to:
    - operation:
        methods: ["GET", "POST", "PUT", "DELETE"]
        paths: ["/api/orders/*"]
    when:
    - key: request.auth.claims[role]
      values: ["user", "admin"]
  
  # Admin-only endpoints
  - from:
    - source:
        principals: ["cluster.local/ns/microservices/sa/admin"]
    to:
    - operation:
        methods: ["GET", "POST", "DELETE"]
        paths: ["/api/admin/*"]
    when:
    - key: request.auth.claims[role]
      values: ["admin"]
  
  # Health checks always allowed
  - to:
    - operation:
        methods: ["GET"]
        paths: ["/actuator/health", "/actuator/ready", "/actuator/live"]

---
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: order-service-jwt
  namespace: microservices
spec:
  selector:
    matchLabels:
      app: order-service
  jwtRules:
  - issuer: "https://auth.company.com"
    jwksUri: "https://auth.company.com/.well-known/jwks.json"
    audiences:
    - "order-service"
    - "microservices"
    forwardOriginalToken: true
    fromHeaders:
    - name: Authorization
      prefix: "Bearer "

Spring Boot + Istio Integration

Configuration Management

Spring Boot Application Configuration:

java
@Configuration
@Profile("istio")
public class IstioConfiguration {
    
    @Bean
    public WebMvcConfigurer istioWebMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new IstioTracingInterceptor());
                registry.addInterceptor(new IstioSecurityInterceptor());
            }
        };
    }
    
    @Bean
    public FilterRegistrationBean<IstioHeaderFilter> istioHeaderFilter() {
        FilterRegistrationBean<IstioHeaderFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new IstioHeaderFilter());
        registration.addUrlPatterns("/api/*");
        registration.setOrder(1);
        return registration;
    }
}

Istio Header Processing:

java
@Component
@Slf4j
public class IstioHeaderFilter implements Filter {
    
    private static final String TRACE_ID_HEADER = "x-trace-id";
    private static final String SPAN_ID_HEADER = "x-span-id";
    private static final String USER_ID_HEADER = "x-user-id";
    private static final String REQUEST_ID_HEADER = "x-request-id";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // Extract Istio injected headers
        String traceId = httpRequest.getHeader(TRACE_ID_HEADER);
        String spanId = httpRequest.getHeader(SPAN_ID_HEADER);
        String userId = httpRequest.getHeader(USER_ID_HEADER);
        String requestId = httpRequest.getHeader(REQUEST_ID_HEADER);
        
        // Set MDC for logging
        if (traceId != null) MDC.put("traceId", traceId);
        if (spanId != null) MDC.put("spanId", spanId);
        if (userId != null) MDC.put("userId", userId);
        if (requestId != null) MDC.put("requestId", requestId);
        
        // Set security context
        if (userId != null) {
            SecurityContextHolder.getContext().setAuthentication(
                new PreAuthenticatedAuthenticationToken(userId, null, Collections.emptyList())
            );
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
            SecurityContextHolder.clearContext();
        }
    }
}

JWT Processing with Istio:

java
@Component
public class IstioJwtProcessor {
    
    @Value("${istio.jwt.header:x-jwt-payload}")
    private String jwtPayloadHeader;
    
    public UserInfo extractUserInfo(HttpServletRequest request) {
        String jwtPayload = request.getHeader(jwtPayloadHeader);
        
        if (jwtPayload == null) {
            throw new SecurityException("JWT payload not found in Istio headers");
        }
        
        try {
            // Istio already validated the JWT, we just need to parse the payload
            String decodedPayload = new String(Base64.getDecoder().decode(jwtPayload));
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode claims = objectMapper.readTree(decodedPayload);
            
            return UserInfo.builder()
                .id(claims.get("sub").asText())
                .email(claims.get("email").asText())
                .roles(extractRoles(claims.get("roles")))
                .groups(extractGroups(claims.get("groups")))
                .build();
                
        } catch (Exception e) {
            log.error("Failed to process JWT payload from Istio", e);
            throw new SecurityException("Invalid JWT payload");
        }
    }
    
    private List<String> extractRoles(JsonNode rolesNode) {
        if (rolesNode == null || !rolesNode.isArray()) {
            return Collections.emptyList();
        }
        
        List<String> roles = new ArrayList<>();
        rolesNode.forEach(node -> roles.add(node.asText()));
        return roles;
    }
}

Circuit Breaking and Fallbacks

Istio Circuit Breaker Integration:

java
@Service
@Slf4j
public class PaymentServiceClient {
    
    private final WebClient webClient;
    private final MeterRegistry meterRegistry;
    private final Counter circuitBreakerActivations;
    
    public PaymentServiceClient(WebClient.Builder webClientBuilder, MeterRegistry meterRegistry) {
        this.webClient = webClientBuilder
            .baseUrl("http://payment-service:8080")
            .build();
        this.meterRegistry = meterRegistry;
        this.circuitBreakerActivations = Counter.builder("circuit_breaker_activations")
            .description("Circuit breaker activations")
            .tag("service", "payment-service")
            .register(meterRegistry);
    }
    
    public Mono<PaymentResult> processPayment(PaymentRequest request) {
        return webClient.post()
            .uri("/api/payments")
            .bodyValue(request)
            .retrieve()
            .onStatus(HttpStatus::is5xxServerError, response -> {
                log.warn("Payment service error, circuit breaker might activate");
                circuitBreakerActivations.increment();
                return Mono.error(new PaymentServiceException("Payment service unavailable"));
            })
            .bodyToMono(PaymentResult.class)
            .timeout(Duration.ofSeconds(10))
            .onErrorMap(WebClientException.class, ex -> {
                log.error("Payment service call failed", ex);
                return new PaymentServiceException("Payment processing failed", ex);
            })
            .doOnSuccess(result -> log.info("Payment processed successfully: {}", result.getPaymentId()))
            .doOnError(error -> log.error("Payment processing failed", error));
    }
    
    public Mono<PaymentResult> processPaymentWithFallback(PaymentRequest request) {
        return processPayment(request)
            .onErrorResume(PaymentServiceException.class, ex -> {
                log.warn("Using fallback for payment processing", ex);
                return Mono.just(createFallbackPaymentResult(request));
            });
    }
    
    private PaymentResult createFallbackPaymentResult(PaymentRequest request) {
        return PaymentResult.builder()
            .paymentId("FALLBACK-" + UUID.randomUUID().toString())
            .orderId(request.getOrderId())
            .status(PaymentStatus.PENDING)
            .message("Payment queued for processing when service is available")
            .timestamp(Instant.now())
            .build();
    }
}

Observability Integration

Tracing and Metrics:

java
@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
    
    private final OrderService orderService;
    private final MeterRegistry meterRegistry;
    private final Timer requestTimer;
    private final Counter requestCounter;
    
    public OrderController(OrderService orderService, MeterRegistry meterRegistry) {
        this.orderService = orderService;
        this.meterRegistry = meterRegistry;
        this.requestTimer = Timer.builder("http_requests_duration")
            .description("HTTP request duration")
            .tag("service", "order-service")
            .register(meterRegistry);
        this.requestCounter = Counter.builder("http_requests_total")
            .description("Total HTTP requests")
            .tag("service", "order-service")
            .register(meterRegistry);
    }
    
    @GetMapping("/{orderId}")
    @Timed(value = "order.get", description = "Get order operation")
    public ResponseEntity<OrderDto> getOrder(@PathVariable Long orderId,
                                           HttpServletRequest request) {
        
        Timer.Sample sample = Timer.start(meterRegistry);
        requestCounter.increment(Tags.of("method", "GET", "endpoint", "/orders/{id}"));
        
        // Extract tracing information from Istio
        String traceId = request.getHeader("x-trace-id");
        String parentSpanId = request.getHeader("x-parent-span-id");
        
        try (MDCCloseable mdcCloseable = MDC.putCloseable("traceId", traceId)) {
            log.info("Processing get order request: orderId={}, traceId={}", orderId, traceId);
            
            OrderDto order = orderService.findById(orderId);
            
            // Add custom span attributes
            Span currentSpan = Span.current();
            currentSpan.setTag("order.id", orderId.toString());
            currentSpan.setTag("order.status", order.getStatus());
            currentSpan.setTag("user.id", order.getUserId().toString());
            
            return ResponseEntity.ok(order);
            
        } catch (OrderNotFoundException e) {
            log.warn("Order not found: orderId={}, traceId={}", orderId, traceId);
            requestCounter.increment(Tags.of("method", "GET", "endpoint", "/orders/{id}", "status", "404"));
            return ResponseEntity.notFound().build();
            
        } catch (Exception e) {
            log.error("Error processing get order request: orderId={}, traceId={}", orderId, traceId, e);
            requestCounter.increment(Tags.of("method", "GET", "endpoint", "/orders/{id}", "status", "500"));
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
            
        } finally {
            sample.stop(requestTimer.tag("method", "GET").tag("endpoint", "/orders/{id}"));
        }
    }
    
    @PostMapping
    @Timed(value = "order.create", description = "Create order operation")
    public ResponseEntity<OrderDto> createOrder(@RequestBody @Valid CreateOrderRequest request,
                                              HttpServletRequest httpRequest) {
        
        String traceId = httpRequest.getHeader("x-trace-id");
        String userId = httpRequest.getHeader("x-user-id");
        
        try (MDCCloseable mdcCloseable = MDC.putCloseable("traceId", traceId)) {
            log.info("Processing create order request: userId={}, traceId={}", userId, traceId);
            
            // Set user context from Istio headers
            request.setUserId(Long.valueOf(userId));
            
            OrderDto createdOrder = orderService.createOrder(request);
            
            // Add custom metrics
            meterRegistry.counter("orders.created", 
                Tags.of("user.segment", determineUserSegment(userId)))
                .increment();
            
            return ResponseEntity.status(HttpStatus.CREATED).body(createdOrder);
            
        } catch (Exception e) {
            log.error("Error creating order: userId={}, traceId={}", userId, traceId, e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

Canary Deployments

Automated Canary Deployment with Flagger:

yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: order-service
  namespace: microservices
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  progressDeadlineSeconds: 60
  service:
    port: 8080
    targetPort: 8080
    gateways:
    - order-gateway
    hosts:
    - order-service.local
  analysis:
    interval: 30s
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 30s
    webhooks:
    - name: acceptance-test
      type: pre-rollout
      url: http://flagger-loadtester.microservices/
      timeout: 30s
      metadata:
        type: bash
        cmd: "curl -sd 'test' http://order-service-canary.microservices:8080/api/health"
    - name: load-test
      url: http://flagger-loadtester.microservices/
      timeout: 5s
      metadata:
        cmd: "hey -z 1m -q 10 -c 2 http://order-service-canary.microservices:8080/api/orders"

Service Mesh vs Application-Level Solutions

Comparison Matrix

FeatureService Mesh (Istio)Application-Level (Spring Cloud)
Language Agnostic✅ Yes❌ Java/JVM only
Infrastructure Separation✅ Proxy-based❌ Library-based
Operations Control✅ Platform team❌ Development team
Learning Curve⚠️ Steep✅ Familiar to Java developers
Debugging⚠️ Complex✅ Easier with IDE support
Resource Overhead⚠️ Sidecar proxy✅ Lower memory footprint
Fine-grained Control⚠️ Configuration-based✅ Code-based
Testing⚠️ Requires cluster✅ Unit/Integration tests
Vendor Lock-in⚠️ Istio-specific⚠️ Spring-specific

Hybrid Approach Implementation

java
@Configuration
@ConditionalOnProperty(name = "servicemesh.enabled", havingValue = "false", matchIfMissing = true)
public class SpringCloudConfig {
    
    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder()
            .filter(circuitBreakerFilter())
            .filter(retryFilter())
            .filter(tracingFilter());
    }
    
    @Bean
    public ExchangeFilterFunction circuitBreakerFilter() {
        return CircuitBreakerFilterFunctions.circuitBreaker("default");
    }
}

@Configuration
@ConditionalOnProperty(name = "servicemesh.enabled", havingValue = "true")
public class ServiceMeshConfig {
    
    @Bean
    public WebClient.Builder webClientBuilder() {
        // Minimal configuration when using service mesh
        return WebClient.builder()
            .filter(headerPropagationFilter());
    }
    
    @Bean
    public ExchangeFilterFunction headerPropagationFilter() {
        return (request, next) -> {
            // Propagate necessary headers for service mesh
            return next.exchange(request);
        };
    }
}

Migration Strategy

java
@Component
@Slf4j
public class ServiceMeshMigrationManager {
    
    @Value("${servicemesh.migration.enabled:false}")
    private boolean migrationEnabled;
    
    @Value("${servicemesh.migration.percentage:0}")
    private int migrationPercentage;
    
    public boolean shouldUseServiceMesh(String serviceName) {
        if (!migrationEnabled) {
            return false;
        }
        
        // Gradual migration based on service hash
        int hash = Math.abs(serviceName.hashCode()) % 100;
        boolean useServiceMesh = hash < migrationPercentage;
        
        log.debug("Service mesh decision for {}: {} (hash: {}, percentage: {})",
            serviceName, useServiceMesh, hash, migrationPercentage);
            
        return useServiceMesh;
    }
    
    @Bean
    @ConditionalOnProperty(name = "servicemesh.migration.enabled", havingValue = "true")
    public WebClient adaptiveWebClient() {
        return WebClient.builder()
            .filter((request, next) -> {
                String serviceName = extractServiceName(request.url());
                
                if (shouldUseServiceMesh(serviceName)) {
                    // Use service mesh configuration
                    return next.exchange(request);
                } else {
                    // Use Spring Cloud configuration
                    return applySpringCloudFeatures(request, next);
                }
            })
            .build();
    }
    
    private Mono<ClientResponse> applySpringCloudFeatures(ClientRequest request, 
                                                         ExchangeFunction next) {
        // Apply circuit breaker, retry, load balancing etc.
        return circuitBreakerFilter().filter(request, next);
    }
}

This comprehensive Service Mesh implementation covers all features of Istio integrated with Spring Boot applications, along with migration strategies and hybrid approaches. Each topic is detailed with production-ready examples and thorough explanations.

Created by Eren Demir.