Spring Boot Admin

Le but de Spring Boot Admin est de centraliser l’accès aux endpoints /actuator d’un ensemble d’applications Spring. C’est particulièrement utile pour des déployements en cluster, lorsque des applications sont déployées sur plusieurs instances.

Dans cette situation, il faut identifier chaque instance et faire de la réécriture de requête. Ça peut se gérer assez facilement dans un environnement à l’ancienne, avec quelques instances et nginx, ou équivalent, configuré de façon statique. Par contre, avec un déploiement plus dynamique, les choses se compliquent. C’est là que Spring Boot Admin est utile, en collectant les informations des différentes instances et centralisant les accès.

Schéma daccès à l’actuator en cluster

Premiers pas avec Spring Boot Admin

Spring Boot Admin est un projet géré par Johannes Edmeier et son ancien employeur Codecentric. Et même si le projet est indépendant de Spring, il est intégré au Spring Initializr.

Spring Initializr avec Spring Boot Admin Server

Installation

Pour utiliser Spring Boot Admin, il faut faire sa propre application, avec le starter de.codecentric:spring-boot-admin-starter-server, en activant le serveur avec l’annotation @EnableAdminServer.

  <dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
  </dependency>
@SpringBootApplication
@EnableAdminServer
public class AdminApplication {

	public static void main(String[] args) {
		SpringApplication.run(AdminApplication.class, args);
	}

}

On va donc pouvoir le personnaliser comme n’importe quelle application Spring Boot, par exemple pour la sécurité et les restrictions d’accès.

Fonctionnalités

Spring Boot Admin a des endpoints pour la liste des applications, la liste des instances et relayer des requêtes à ces endpoints.

Il a aussi une interface graphique, qu’on peut supprimer en excluant la dépendance.

  <dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <exclusions>
      <exclusion>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-server-ui</artifactId>
      </exclusion>
    </exclusions>
  </dependency>

Configuration

La configuration de notre application d’admin est similaire à une application classique: port, sécurité, logging,…​ Les propriétés propres à Spring Boot Admin sont dans la catégorie spring.boot.admin.

La seule propriété qui doit être configurée concerne CORS. En effet, sans elle le header origin est transmis entre l’application d’admin et le endpoint d’actuator, ce qui peut causer des erreurs 403 CORS.

Enregistrement des instances

Pour fonctionner, notre application d’admin doit connaître les instances auxquelles il devra transmettre les requêtes.

Pour les connaître, il y a 3 logiques:

  • chaque instance s’enregistre auprès du serveur,

  • le serveur est configuré pour connaître les instances,

  • le serveur interroge un registre comme Eureka, Consul ou K8S pour y récupèrer la liste des instances.

Spring Boot Admin client

Dans cette configuration, chaque instance vient s’enregistrer auprès du serveur SBA. Pour ça, on doit ajouter la dépendance au client SBA à chaque application.

  <dependency>
      <groupId>de.codecentric</groupId>
      <artifactId>spring-boot-admin-starter-client</artifactId>
  </dependency>

Ensuite on configure l’URL du serveur SBA auquel elle doit s’enregistrer.

spring:
  boot:
    admin:
      client:
        url: http://localhost:9999

Ainsi, chaque instance envoie des informations pour que le serveur puisse se connecter, collecter des informations et transmettre les requêtes à l'`/actuator`.

Par défaut, le client envoie des informations de connexion basée sur le canonical hostname en utilisant la méthode InetAddress::geCanonicalHostName. Le problème classique, c’est que ce nom peut être un nom privé, impossible à exploiter pour le serveur. A la place, on peut envoyer l’adresse IP, avec la propriété spring.boot.admin.client.instance.service-host-type.

spring:
  boot:
    admin:
      client:
        instance
          service-host-type: IP

Si l’actuator est sécurisé, le client doit envoyer les informations au serveur, dans les métadonnées de l’instance (spring.boot.admin.client.instance.metadata).

spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        instance:
          metadata:
            user.name: monitor
            user.password: monitor-pwd

Si le serveur est sécurisé, le client doit avoir les informations d’authentification.

spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        username: admin
        password: admin-pwd

Synthèse: configuration complète du client SBA

spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        username: admin
        password: admin-pwd
        instance:
          service-host-type: IP
          metadata:
            user.name: monitor
            user.password: monitor-pwd

Découverte statique

Avec cette solution, la liste des instances et leurs données de connexion est directement dans la configuration du serveur. Cette solution est simple à implémenter mais a l’inconvénient d’être purement statique, et donc contraire aux objectifs.

Même pour cette solution statique, il faut que le serveur ait une dépendance vers Spring Cloud.

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
  </dependency>

Il n’y a rien à configurer coté instance. Du coté du serveur, il faut ajouter les informations de connexion, avec éventuellement des informations d’authentification.

spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            api:
              - uri: https://api1.example.com
                metadata:
                  user.name: monitor
                  user.password: monitor-pwd
              - uri: https://api2.example.com

Découverte Kubernetes

Si l’application est déployée sur un Kubernetes, Spring est capable de découvrir la topologie du cluster grâce à l’API de Kubernetes.

Pour exploiter cette possibilité, il faut que le serveur SBA ait une dépendance vers le service de découverte pour Kubernetes.

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-all</artifactId>
  </dependency>

L’ajout du starter suffit à activer la découverte. Il faut toutefois que les permissions soient activées au niveau de l’API Kubernetes (commande testée avec Minikube, sur le namespace par défaut).

~$ kubectl create clusterrolebinding make-default-sa-cluster-admin \
           --serviceaccount=default:default --clusterrole=cluster-admin

Il faut aussi activer le scheduling pour que la recherche des instances se fasse à intervalle régulier.

@SpringBootApplication
@EnableAdminServer
@EnableScheduling
public class AdminApplication {

	public static void main(String[] args) {
		SpringApplication.run(AdminApplication.class, args);
	}

}

Enfin, il faut filtrer les services par label afin de ne récupérer que les nodes Spring qui ont un actuator.

spring:
  cloud:
    kubernetes:
      discovery:
        service-labels:
          category: application

Si les actuateurs sont sécurisés, on peut renseigner le nom d’utilisateur et le mot de passe au niveau global, pour toutes les instances.

spring:
  boot:
    admin:
      instance-auth:
        default-user-name: monitor
        default-password: monitor-pwd

La configuration spécifique d’une instance est prioritaire par rapport à cette configuration globale. Pour ça, il faut ajouter les informations au niveau des labels du service Kubernetes.

apiVersion: v1
kind: Service
metadata:
  name: app1
  labels:
    category: application
    user.name: monitor
    user.password: monitor-psswd

Attention, il faut bien mettre user.name ET user.password. Avec un seul des deux, il sera ignoré.

API

Je n’ai pas trouvé de documentation de l’API. On peut la découvrir en parcourant l’interface graphique ou en parcourant le code.

J’ai essayé d’ajouter springdoc, mais il ne détecte pas automatiquement les endpoints.

Applications

  • GET {{admin.url}}/applications
    liste des applications avec la liste des instances par application

  • GET {{admin.url}}/applications/{name}
    détail d’une application, avec la liste de ses instances

  • XXX {{admin.url}}/applications/{name}/actuator/{endpoint}
    endpoint de l’actuator d’une application, la requête est envoyée à chaque instance et la réponse aggrège les différentes réponses

Instances

  • GET {{admin.url}}/instances
    liste des instances

  • GET {{admin.url}}/instances?name={name}
    liste des instances, peut être limitée à une seule application avec le paramètre name

  • GET {{admin.url}}/instances/{id}
    détail d’une instance

Actuators

Le serveur SBA fonctionne en relais entre le client et l’actuateur. Par conséquents les endpoints disponibles dépendent des endpoints activés pour chaque application et instance.

Les endpoints sont généralement appelés pour une instance.

  • XXX {{admin.url}}/instances/{id}/actuator/{endpoint}
    endpoint de l’actuator d’une instance, quelle que soit la méthode GET, POST,…​; la racine n’est pas accessible

Exemple pour /loggers

  • GET {{admin.url}}/instances/{id}/actuator/loggers
    liste et configuration des loggers de l’instance

  • GET {{admin.url}}/instances/{id}/actuator/loggers/{logger.name}
    configuration d’un' logger de l’instance

  • POST {{admin.url}}/instances/{id}/actuator/loggers/{logger.name}
    modification de la configuration d’un' logger de l’instance

Il y a aussi la possibilité d’appeler un endpoint pour une application. Dans ce cas, la requête est transmise à chaque instance et la réponse est aggrégée.

  • XXX {{admin.url}}/applications/{name}/actuator/{endpoint}
    endpoint de l’actuator d’une application

Exemple pour /loggers

  • POST {{admin.url}}/applications/{name}/actuator/loggers/{logger.name}
    modification de la configuration d’un' logger pour toutes les instances de l’application

Tips

CORS

Le header Origin est transmis à l’instance, ce qui peut poser des erreurs CORS sur les POST

Il y a 2 solutions:

  • ajouter SBA dans les URLs autorisées par l’instance,

  • empécher le transfert du header Origin.

Je préfère largement la 2°, d’autant qu’elle est assez simple à mettre en oeuvre, coté SBA, avec la propriété spring.boot.admin.instance-proxy.ignored-headers.

spring:
  boot:
    admin:
      instance-proxy:
        ignored-headers: Cookie, Set-Cookie, Authorization, Origin

La valeur par défaut de cette propriété est Cookie, Set-Cookie, Authorization, j’y ajoute Origin.

Il faut noter que les headers suivants sont ignorés quelle que soit la configuration, car rangés dans la catégorie hop_by_hop: "Host", "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "X-Application-Context".

oauth2

L’authentification par oauth2 n’est que partiellement supportée.

SBA peut être sécurisé en utilisant une authentification oauth2 avec une validation des token par JWKS.

Par contre pe n’ai rien trouvé pour l’interface graphique concernant le support d’authentification oauth2 et de token. De plus, en enregistrement par le client il faut un autre mode.

De même pour la sécurisation des actuators, si on utilise oauth2, il faut aussi un mode annexe car SBA vient interroger périodiquement les endpoints de l’actuator, indépendamment de toute requête d’utilisateur.

ID d’instances

Les IDs des instances survivent au redémarrage de SBA. Ils sont calculés à partir de l’URL du service. C’est le SHA-1 de de l’URL de health, en hexadécimal.