Accede al API de Kubernetes desde un Pod con Java
Introducción
En este ejemplo os mostraré cómo podéis acceder a la API REST de vuestro Kubernetes cluster desde un Pod utilizando YAKC (Yet Another Kubernetes Client), Eclipse JKube y Spring Boot.
En la primera parte os enseñaré cómo crear una aplicación muy sencilla basada en Spring Boot con la dependencia a YAKC Kubernetes Client (ver introducción a YAKC).
En la segunda parte describo cómo desplegar la aplicación en un cluster de Kubernetes y cómo hacer peticiones a los endpoints REST expuestos en el Servicio del Pod para obtener información del Cluster.
Aplicación de ejemplo
Lo primero que haremos será crear una aplicación Spring Boot the ejemplo. Este es el fichero pom.xml que utilizaremos. Además de las dependencias estándar para Spring Boot, también debemos de incluir las dependencias de YAKC Kubernetes Client (y su api):
1<!-- ... -->
2 <dependencies>
3<!-- ... -->
4 <dependency>
5 <groupId>com.marcnuri.yakc</groupId>
6 <artifactId>kubernetes-api</artifactId>
7 <version>${project.version}</version>
8 </dependency>
9 <dependency>
10 <groupId>com.marcnuri.yakc</groupId>
11 <artifactId>kubernetes-client</artifactId>
12 <version>${project.version}</version>
13 </dependency>
14 </dependencies>
15<!-- ... -->
Ahora podemos declarar un Bean de KubernetesClient
en la main class de nuestra aplicación (AccessClusterFromPodApplication
):
1/* ... */
2 @Bean(destroyMethod = "close")
3 public KubernetesClient kubernetesClient() {
4 return new KubernetesClient();
5 }
6/* ... */
Este Bean nos permitirá inyectar (autowire) la instancia compartida (singleton) de KubernetesClient
al resto de componentes de la aplicación.
Cluster Service
Primero definiremos los distintos métodos que vamos a necesitar en la interfaz ClusterService
. A continuación definiremos su implementación en la clase KubernetesClientClusterService
.
El método más complejo de la clase es la implementación del método getDeploymentLogs
. Este método se encarga de consultar y agregar las trazas (ordenadas por fecha/hora) de todos los Pods de un Deployment específico. Describiré con más detalle esta implementación debido a su complejidad. El resto de métodos deberían de ser evidentes en el código.
1@Override
2 public String getDeploymentLogs(String deploymentName) {
3 try {
4 final Deployment deployment = kubernetesClient.create(AppsV1Api.class)
5 .readNamespacedDeployment(deploymentName, getNamespace()).get();
6 return kubernetesClient.create(CoreV1Api.class).listNamespacedPod(getNamespace(),
7 new ListNamespacedPod().labelSelector(
8 deployment.getSpec().getSelector().getMatchLabels().entrySet().stream()
9 .map(e -> String.format("%s=%s", e.getKey(), e.getValue())).collect(
10 Collectors.joining(","))
11 )).stream()
12 .flatMap(this::getPodLog)
13 .sorted()
14 .collect(Collectors.joining("\n"));
15 } catch(NotFoundException e) {
16 throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
17 } catch (IOException e) {
18 throw new ServerErrorException(e.getMessage(), e);
19 }
20 }
21
22 private Stream<String> getPodLog(Pod pod) {
23 try {
24 return Stream.of(kubernetesClient.create(CoreV1Api.class)
25 .readNamespacedPodLog(pod.getMetadata().getName(), getNamespace(), new ReadNamespacedPodLog().timestamps(true))
26 .get().split("\n")
27 ).map(logLine -> {
28 final String[] splitLine = logLine.split("\\s", 2);
29 return String.format("%s [%s] - %s", splitLine[0], pod.getMetadata().getName(),
30 splitLine.length > 1 ? splitLine[1] : "");
31 });
32 } catch (IOException e) {
33 throw new ServerErrorException("Cannot retrieve log", e);
34 }
35 }
El método comienza creando un cliente para la API de Apps V1. Utilizaremos esta API para consultar información del Deployment correspondiente al nombre proporcionado en el namespace configurado.
A continuación emplearemos las entradas del campo spec.selector.matchLabels
del Deployment obtenido para obtener la lista de Pods que concuerdan con este selector. Convertimos la lista de Pods en un Stream al que hacemos flatMap
para mapearlos en un stream de entradas en el log de cada uno de los Pods. La implementación de este mapeo la vemos en el método getPodLog
.
El método getPodLog
utiliza la API Core V1 para leer las entradas de los logs de un determinado Pod incluyendo los timestamps. A continuación transformamos cada una de las trazas obtenidas para dividirlas por separadores de línea y así poder incluir el nombre del Pod en cada una de las líneas después del timestamp.
Finalmente, en el método getDeploymentLogs
una vez el mapeo de los logs se ha completado, se ordenan las entradas y se combinan en un único String que será el resultado del método.
A pesar de la complejidad que supone transformar, combinar y ordenar las trazas de todos los Pods correspondientes a un Deployment, las interfaces declarativas y “streamable” de YAKC lo convierten en una tarea sencilla. La implementación resultante también es muy legible y se puede conseguir con muy pocas líneas de código.
Cluster Resource
En la clase ClusterResource puedes ver la implementación de los distintos endpoints REST que expondrá la aplicación. Esta clase es un RestController
estándar de Spring. La clase contiene distintos métodos anotados con GetMapping
que permiten consultar el nombre del namespace configurado, los Deployments y Pods en este namespace, así como las trazas agregadas de todos los Pods de un Deployment determinado.
Por ejemplo, para el método que he descrito en la sección anterior, podemos hacer una petición GET a http://cluster-ingress-url/deployments/deployment-name/logs
y así obtener el Log agregado para el Deployment especificado:
1@GetMapping("deployments/{deployment}/logs")
2 public String getDeploymentLogs(@PathVariable("deployment") String deployment) {
3 return clusterService.getDeploymentLogs(deployment);
4 }
Desplegando la aplicación en el Cluster
Una vez hemos destripado la aplicación y hemos visto como utilizar YAKC para interactuar con el Cluster, es el momento de desplegarla.
Lo primero que debemos hacer es configurar el cluster para permitir que el Pod pueda acceder a la API del cluster. Para ello debemos proporcionar acceso al service account por defecto del Pod. Esto lo podemos conseguir ejecutando el siguiente comando:
1kubectl create clusterrolebinding default-cluster-admin --clusterrole cluster-admin --serviceaccount=default:default
A partir de este momento, el service account por defecto (credenciales proporcionadas en la ruta /var/run/secrets/kubernetes.io/serviceaccount/token
) del Pod podrá acceder a la API REST del cluster.
Eclipse JKube
Para desplegar la aplicación utilizaré el Kubernetes Maven Plugin de Eclipse JKube ya que nos permite desplegar cualquier aplicación Java con una mínima configuración.
Configuramos el plugin en la sección build del pom.xml
:
1<!-- ... -->
2<plugin>
3 <groupId>org.eclipse.jkube</groupId>
4 <artifactId>kubernetes-maven-plugin</artifactId>
5 <version>1.17.0</version>
6 <executions>
7 <execution>
8 <id>jkube</id>
9 <phase>package</phase>
10 <goals>
11 <goal>build</goal>
12 <goal>resource</goal>
13 <goal>apply</goal>
14 </goals>
15 </execution>
16 </executions>
17 <configuration>
18 <enricher>
19 <config>
20 <jkube-service>
21 <type>NodePort</type>
22 </jkube-service>
23 </config>
24 </enricher>
25 </configuration>
26</plugin>
27<!-- ... -->
He vinculado las tareas build, resource y apply del plugin a la fase package
. Con esto conseguiremos que la imagen Docker de la aplicación de construya y despliegue en el cluster automáticamente cuando ejecutemos el comando mvn package
.
También podéis observar que he añadido una configuración para los enrichers de JKube de manera que el Service generado sea de tipo NodePort
.
Si vamos a desplegar la aplicación en Minikube, es conveniente compartir el acceso al Docker daemon de Minikube a nuestro sistema anfitrión. Esto nos permitirá no tener que publicar la imagen construida en un registro compartido.
eval $(minikube docker-env)
on Linux@FOR /f "tokens=*" %i IN ('minikube -p minikube docker-env') DO @%i
on Windows
Una vez configurado el entorno, podemos proceder a desplegar la aplicación (mvn clean package
):
Si todo ha ido bien, la aplicación debería de estar ahora desplegada. Si estamos utilizando Minikube, podemos probar los endpoints de la aplicación ejecutando los siguientes comandos:
Conclusión
En esta publicación os he mostrado como podemos acceder con facilidad a la API REST de Kubernetes desde una aplicación Java corriendo en un Pod mediante YAKC (Yet Another Kubernetes Client). También os he mostrado como construir la imagen Docker de la aplicación y como desplegarla en el cluster con una mínima configuración empleando el Plugin de Maven para Kubernetes de Eclipse JKube.
Puedes encontrar el código fuente completo de este post en la sección quickstarts del repositorio de GitHub de YAKC.