Notre

blog

Kubernetes et Spring Boot 3 : mettez en place un HPA basé sur des custom metrics

Kubernetes et Spring Boot 3 : mettez en place un HPA basé sur des custom metrics

L’Horizontal Pod Autoscaler (HPA) est devenu un composant incontournable de Kubernetes pour adapter automatiquement le nombre de pods en fonction de la charge. Par défaut, il s’appuie sur des métriques standard comme l’utilisation du CPU ou de la mémoire.

Dans un environnement Spring Boot, ces métriques sont pourtant loin d’être idéales. Le fonctionnement de la JVM peut entraîner des montées en charge artificielles ou empêcher un retour à la normale, conduisant à un autoscaling inefficace.

Nous allons voir comment mettre en place un HPA basé sur une métrique métier, en utilisant Spring Boot 3, Micrometer, Prometheus et Prometheus Adapter.

À la fin de cet article, vous saurez :

  • exposer les métriques de votre application Spring Boot ;
  • sécuriser leur accès ;
  • les collecter avec Prometheus ;
  • créer une métrique personnalisée :
  • l’utiliser pour piloter votre HPA.

Pourquoi les métriques CPU et mémoire ne suffisent pas avec Spring Boot ?

Sur le papier, utiliser le CPU ou la mémoire semble logique. En pratique, avec une application Java cela pose plusieurs problèmes.

Lors du démarrage d’un pod, la JVM sollicite fortement le processeur afin de charger les classes, compiler le code (JIT) et initialiser l’application. Le HPA peut interpréter cette consommation comme une montée en charge et créer de nouveaux pods… qui reproduiront le même comportement.

La mémoire présente également ses limites. Une fois qu’elle a été allouée à la JVM, elle est rarement restituée au système avant l’arrêt du processus. Même lorsque la charge diminue, Kubernetes peut continuer à considérer que les pods consomment beaucoup de mémoire et retarder, voire empêcher, le scale down.

Le résultat est alors peu représentatif de la charge réelle de votre application.

Pourquoi ?

Un HPA est uniquement aussi pertinent que les métriques qu’il exploite. Si ces métriques ne reflètent pas l’expérience utilisateur ou l’activité réelle de l’application, les décisions d’autoscaling seront inadaptées.

Une alternative consiste à utiliser des Custom Metrics, par exemple :

  • la latence moyenne des requêtes HTTP ;
  • le nombre de requêtes par seconde ;
  • la longueur d’une file Kafka ou RabbitMO ;
  • toute métrique métier exposée par votre application.

Dans cette article, nous utiliserons la latence HTTP moyenne.

Étape 1 : Exposer les métriques Spring Boot

2 étapes indispensables : charger les dépendances actuator et micrometer puis exposer les endpoints nécessaires.

Charger les dépendances dans le fichier pom.xml

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
 <groupId>io.micrometer</groupId>
 <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Activer l’endpoint de metrics prometheus dans application.yml management :

# endpoint for metrics
metrics:
 export:
  prometheus:
   enabled: true
# endpoint for healthcheck: readiness and liveness
endpoint:
 health:
  probes:
   enabled: true
# exposition of endpoints
endpoints:
 web:
  exposure:
   include: health,prometheus,configprops

À ce stade, les metrics de notre application springboot devrait être accessible via l’url /actuator/prometheus

Étape 2 : Protéger l’endpoint /actuator/prometheus

Facultatif, mais recommandé : l’endpoint /actuator/prometheus ne doit pas être publiquement accessible.

On va donc le protéger via un basicAuth.

Définir des paramètres dans application.yaml :

metrics-security:
 name: ${PROMETHEUS_USERNAME:prometheus}
 password: ${PROMETHEUS_PASSWORD:tobemodified}
roles: MONITORING

Chargement des paramètres (exemple : package : config, class : MetricsSecurityConfiguration)

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
@ConfigurationProperties(prefix = "metrics-security")
public class MetricsSecurityConfiguration{
 private String name;
 private String password;
 private String roles;
}

Définition d’un SecurityFilterChain de type basicAuth dans le fichier de configuration de Spring Security (dans notre cas, package : config, class : SecurityConfiguration)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableM
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebS
import org.springframework.security.config.annotation.web.configurers.AbstractHttp
import org.springframework.security.config.annotation.web.configurers.HeadersConfi
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@RequiredArgsConstructor
@EnableWebSecuritypublic class SecurityConfiguration {

 @Bean
 public UserDetailsService userDetailsService(final MetricsSecurityConfiguratioUserDetails user = User.builder()
 .username(metricsSecurityConfiguration.getName())
 // {noop} indique que le mot de passe est en clair (No Operation E
 .password("{noop}" + metricsSecurityConfiguration.getPassword())
 .roles(metricsSecurityConfiguration.getRoles())
 .build();
  
   return new InMemoryUserDetailsManager(user);

}

private final AccountService accountService;

// --- 1. CHAÎNE DÉDIÉE À PROMETHEUS (Priorité haute) ---
@Bean
@Order(1) // S'exécute avant la chaîne par défaut
public SecurityFilterChain prometheusFilterChain(HttpSecurity http) throws Exchttp
 .securityMatcher("/actuator/prometheus")
 .authorizeHttpRequests(
   authorize ->
   authorize.anyRequest().hasRole("MONITORING")
 )
 .httpBasic(Customizer.withDefaults())
 .csrf(AbstractHttpConfigurer::disable) // CSRF is disabled for metrics
 .sessionManagement(session -> session.sessionCreationPolicy(SessionCre

 return http.build();
 }
}

Pourquoi ?

Les endpoints Actuator constituent une source d’information précieuse pour un attaquant. Restreindre leur accès est une bonne pratique de sécurité, même dans un cluster Kubernetes privé.

Étape 3 : Déclarer un ServiceMonitor

Le serviceMonitor est l’objet Kubernetes permettant à prometheus de connaître les endpoints à contacter afin de collecter les métriques.

Voici un exemple pour notre application :

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
 labels:
  app.kubernetes.io/component: component
  app.kubernetes.io/instance: instance
  app.kubernetes.io/name: application
name: application
spec:
 endpoints:
 - basicAuth:
  password:
   key: password
   name: prometheus-security
  username:
   key: username
   name: prometheus-security
interval: 10s
path: /actuator/prometheus
relabelings:
 - action: replace
  separator: ':'
  sourceLabels:
  - __meta_kubernetes_service_label_app_kubernetes_io_name
  - __meta_kubernetes_service_label_app_kubernetes_io_component
  targetLabel: app
 scheme: http
jobLabel: application
namespaceSelector:
 matchNames:
 - application
selector:
 matchLabels:
  app.kubernetes.io/component: component
  app.kubernetes.io/instance: instance
  app.kubernetes.io/name: application

Après son déploiement, Prometheus commencera automatiquement à collecter les métriques de votre application.

Pourquoi ?

Sans ServiceMonitor, Prometheus ignore totalement l’existence de votre application. Cette ressource fait le lien entre votre service Kubernetes et votre plateforme de monitoring.

Étape 4 : Déployer Prometheus Adapter et créer un custom metric

À ce stade, Prometheus collecte bien les métriques. Pourtant Kubernetes ne peut toujours pas les utiliser. En effet, le HPA ne dialogue pas directement avec Prometheus. Il interroge l’API Kubernetes custom.metrics.k8s.io/v1beta1 . C’est précisément le rôle de Prometheus Adapter.

Nous allons le déployer en utilisant le helm et utiliser le fichier values.yaml pour provisionner nos métriques.

values.yaml

# URL d'accés à promtheus
prometheus:
 url: http://kube-prometheus-kube-prome-prometheus.prometheus.svc
 port: 9090
 path: ""

rules:
 # Définition des custom metrics
 custom:
 ## Définition de la métrique "http_latency_average_springboot"
 - seriesQuery: '{__name__=~"http_server_requests_seconds_count",namespace!="",po
resources:
  overrides:
   namespace: {resource: "namespace"}
   pod: {resource: "pod"}
 name:
  as: "http_latency_average_springboot"
metricsQuery: |
 (
  sum(
   rate(
    http_server_requests_seconds_sum{uri!~"/actuator/.*", <<.LabelMatchers
   )
  ) by (<<.GroupBy>>)
  /
  sum(
   rate(
    http_server_requests_seconds_count{uri!~"/actuator/.*", <<.LabelMatche
   )
  ) by (<<.GroupBy>>)
)
or
(
 sum(http_server_requests_seconds_count{<<.LabelMatchers>>}) by (<<.GroupBy
)

Commande de déploiement

helm install prometheus-adapter \
 oci://ghcr.io/prometheus-community/charts/prometheus-adapter \
 -f values.yaml

Une fois déployé et après quelques minutes, l’api custom.metrics.k8s.io/v1beta1 sera accessible via la commande.

kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1

Ou pour interroger précisément un pod :

kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/${namespace-name}

Pourquoi ?

Dans ce fichier values.yaml , nous avons créé une métrique nommée http_latency_average_springboot . Celle-ci est calculée de la manière suivante :

  • Somme du temps total des requêtes ( http_server_requests_seconds_sum )
  • Divisé par le nombre total de requêtes ( http_server_requests_seconds_sum )
  • Le tout en ignorant l’uri /actuator et ne prenant en compte que les 2 dernières minutes ( [2m] )
  • On ajoute une opération or qui force un résultat à 0 si le premier calcul échoue. (En cas d’absence de requête, on obtient une division par 0 pour le premier calcul, ce qui cause une erreur)

Étape 5 : Configurer le Horizontal Pod Autoscaler (HPA)

Il ne reste plus qu’à configurer le HPA.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
 labels:
  app.kubernetes.io/component: component
  app.kubernetes.io/instance: instance
  app.kubernetes.io/name: application
 name: application
spec:
 behavior:
  scaleDown:
   policies:
   - periodSeconds: 60
   type: Pods
   value: 1
  selectPolicy: Max
  stabilizationWindowSeconds: 300
 scaleUp:
  policies:
  - periodSeconds: 15
   type: Podsvalue: 4
  - periodSeconds: 15
   type: Percent
   value: 100
   selectPolicy: Max
   stabilizationWindowSeconds: 0
maxReplicas: 3
metrics:
- resource:
  name: cpu
  target:
   averageUtilization: 85
   type: AverageValue
 type: Resource
- pods:
 metric:
  name: http_latency_average_springboot
 target:
  averageValue: 500m
  type: AverageValue
 type: Pods
minReplicas: 2
scaleTargetRef:
 apiVersion: apps/v1
 kind: Deployment
name: application

Avec ce HPA, Kubernetes initiera un scaleUp si l’utilisation moyenne du CPU des pods dépasse 85% ou si la latence http moyenne dépasse 500ms.

Pourquoi ?

Combiner plusieurs métriques permet d’obtenir un comportement plus robuste. Le CPU reste un indicateur utile dans certaines situations tandis que la latence reflète directement l’impact sur les utilisateurs.

En résumé

Les métrics CPU et mémoire sont simples à mettre en oeuvre, mais elles ne constituent pas toujours les meilleurs indicateurs pour des applications Spring Boot.

En s’appuyant sur des Custom Metrics, Kubernetes peut prendre des décisions beaucoup plus pertinentes et adapter les ressources en fonction de la charge réelle de l’application plutôt que du comportement de la JVM.

La combinaison Spring Boot 3 + Micrometer + Prometheus + Prometheus Adapter + HPA permet ainsi de construire un autoscaling plus précis, plus stable et mieux aligné avec l’expérience utilisateur.

Pour les équipes DevOps et les architectes cloud, cette approche représente une excellente manière d’améliorer les performances tout en optimisant la consommation des ressources du cluster Kubernetes.