# Java 操作 Kubernetes

起因:公司准备将应用容器化部署,先使用了华为云的 Kubernetes 服务,后面又使用阿里云的 Kubernetes 服务。并短期一个月内无法判断走哪个云商。而作为一个在公司内部用于应用发布,部署的应用。在对接完华为云的 Kubernetes 服务 Api 后。再对接阿里云发现阿里云并没用像华为云一样对 KubernetesApi 做简易的封装。其两者的区别是华为云可以通过 ak , sk 再加 Kubernetes Api 获取数据。可以理解为华为云多了一层代理。

# 添加依赖

本篇使用的是官方维护的 Kubernetes Java Client 包 。有兴趣的可以了解下面的社区维护版

官方 SDK

官方 JAVA SDK GitHub

建议使用最新版本 maven 中央仓库

<dependency>
    <groupId>io.kubernetes</groupId>
    <artifactId>client-java</artifactId>
    <version>13.0.0</version>
</dependency>

# 准备 kubeconfig 文件

用于配置集群访问的文件称为 kubeconfig 文件。默认情况下, kubectl$HOME/.kube 目录下查找名为 config 的文件。什么是 kubeconfig

# 阿里云

容器服务 - Kubernetes 进入集群 集群信息 连接信息。复制内容

# 华为云

云容器引擎 资源管理 集群管理 进入集群 基本信息右边 kubectl 下载 kubectl 配置文件

可以使用 lens 工具管理 Kubernetes 集群

# Java 连接 Kubernetes

首先将这个内容文件重命名为 config 。

使用上:官方例子获取所有 NameSpaces 下的 Pod。

package io.kubernetes.client.examples;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.ApiException;
import io.kubernetes.client.Configuration;
import io.kubernetes.client.apis.CoreV1Api;
import io.kubernetes.client.models.V1Pod;
import io.kubernetes.client.models.V1PodList;
import io.kubernetes.client.util.ClientBuilder;
import io.kubernetes.client.util.KubeConfig;
import java.io.FileReader;
import java.io.IOException;
/**
 * A simple example of how to use the Java API from an application outside a kubernetes cluster
 *
 * <p>Easiest way to run this: mvn exec:java
 * -Dexec.mainClass="io.kubernetes.client.examples.KubeConfigFileClientExample"
 *
 */
public class KubeConfigFileClientExample {
  public static void main(String[] args) throws IOException, ApiException {
    //file path to your KubeConfig (看你自己 config 文件放哪)
    String kubeConfigPath = "~/.kube/config";
    // loading the out-of-cluster config, a kubeconfig from file-system
    ApiClient client =
        ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build();
    // set the global default api-client to the in-cluster one from above
    Configuration.setDefaultApiClient(client);
    // the CoreV1Api loads default api-client from global configuration.
    CoreV1Api api = new CoreV1Api();
    // invokes the CoreV1Api client
    V1PodList list = api.listPodForAllNamespaces(null, null, null, null, null, null, null, null, null);
    System.out.println("Listing all pods: ");
    for (V1Pod item : list.getItems()) {
      System.out.println(item.getMetadata().getName());
    }
  }
}

但我不想使用第三方云存储如阿里云的 OSS 或者在 Jar 包的相对路径下放置一份 config 文件。所以我会把这个文件放在 Jar 包内。也就是我放在 resources 下的自建目录 kubernetes 下,所以改动如下。

// import sun.security.util.Resources;
BufferedReader reader = new BufferedReader(new InputStreamReader(Resources.class.getResourceAsStream("/kubernetes/config")));
ApiClient client = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(reader)).build();

因为当打成 Jar 包后,文件就在包内部了。

new FileReader(path) 实例化内部底层会 new File(path) 在获取 InputStream 。而 File 这个路径是相对于包的相对路径,并不是指向包内的文件。如果继续使用会出现 idea 跑项目时没有问题,打包部署时出错

# 初始化 ApiClient

因为有四个环境,不能像官方例子使用 Configuration.setDefaultApiClient() 设置默认 client

public class KubernetesUtil {
  public final static Map<String,ApiClient> alyClusterMap = new HashMap<String, ApiClient>(){
    {
      try {
         BufferedReader reader = IoUtil.getReader(new ClassPathResource("kubernetes/aly-dev-config").getInputStream(), "utf-8");
         put("DEV",ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(reader)).build());
         put("TEST",null);
         put("PRE",null);
         put("PROD",null);
      } catch (IOException e) {
         e.printStackTrace();
      }
    }
  };
}

# 例:获取 pod 信息

首先通过 Client 获取 CoreV1Api 对象用于调用相关接口

这里要提一句:Java Kubernetes SKD 操 Api 不仅只有 CoreV1Api 这个类。如操作 Deployment 时用的是 AppsV1Api 。使用什么我是先到 Kubernetes Api 下找。然后将请求路径在 SDK 源码中查。如果有更好的方法,还请多多提点下 (●’◡’●)

public static List<JSONObject> getPodsByAppName(String appName,String env) {
    // 按需获取所需要的 Client 
    CoreV1Api api = new CoreV1Api(KubernetesUtil.alyClusterMap.get(env));
    V1PodList list = null;
    try {
        list = api.listPodForAllNamespaces(null,null, null, "name="+appName+"-pod", null, null, null, null, null, null);
    } catch (ApiException e) {
        e.printStackTrace();
    }
    // JSONbject 是 Map 对象的实现类。来源是 Hutool 工具
    List<JSONObject> jsonObjectList = list.getItems().parallelStream().map(m -> {
        JSONObject jsonObject = new JSONObject();
        jsonObject.putOnce("podName", m.getMetadata().getName());
        jsonObject.putOnce("podIp", m.getStatus().getPodIP());
        V1Container v1Container = m.getSpec().getContainers().get(0);
        String[] images = v1Container.getImage().split(":");
        jsonObject.putOnce("imageVersion", images[images.length - 1]);
        // 1048576 = 1024 * 1024 因为单位为 Byte 要转换为 MB。 因为获取到的是字节,需要转成 Mb
        jsonObject.putOnce("specification", String.format("CPU:%s | MEM:%s",
			v1Container.getResources().getLimits().get("cpu").getNumber(),
        	v1Container.getResources().getLimits().get("memory").getNumber().divide(new BigDecimal(1048576))));
        jsonObject.putOnce("podStatus", m.getStatus().getPhase());
        Optional.ofNullable(m.getStatus().getStartTime()).ifPresent(p -> {
            // 时间是 UTC 的零时区。不是东八区
            jsonObject.putOnce("startTime", p.plusHours(8).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        });
        return jsonObject;
    }).collect(Collectors.toList());
    return jsonObjectList;
}

# 例:获取 pod 内 Java 启动日志

因为业务要求日志是从上往下看的。所以日志这块 Kubernetes 的参数有点不搭,所以就用 sinceSeconds 。意思是距离现在最近的多少秒日志

接口详情

public static StartLogVO getAppServerLogs(int startDate,String env,String podName,String endLog){
    CoreV1Api api = new CoreV1Api(KubernetesUtil.alyClusterMap.get(env));
    StartLogVO startLogVO = new StartLogVO("",0,null);
    try {
        int now = (int)LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        startLogVO.setStartDate(now +"");
        // 加 5 秒防止日志丢失
        int i =  now - startDate + 5;
        String logs = api.readNamespacedPodLog(podName, "default", null, null, null, null, "true", null, i, null, null);
        startLogVO.setLineNum(logs.substring(logs.length()-20));
		// 去除重复日志
        if(StrUtil.isNotBlank(endLog)){
            int end = logs.indexOf(endLog);
            if(end!=-1){
                logs = StrUtil.removePrefix(logs.substring(end),endLog);
            }
        }
        startLogVO.setLogs(Optional.ofNullable(logs).orElse(""));
    } catch (ApiException e) {
        e.printStackTrace();
    }
    return startLogVO;
}

# 总结

因为华为云通过 aksk 再加 Kubernetes Api 请求地址。我这边仅需发起 HTTPS 请求就行。以至于对接阿里云还需要使用 KubernetesSDK 有些抗拒。还要载入 kubeconfig 文件。最后不想学阿里云例子通过调用系统命令 curl 。所以还是使用了,结果第一次使用就报 NoSuchMethodError kotlin.collections.ArraysKt.copyInto([B[BIII)[B 这个错误,一看报没方法错误,赶紧看下 Mavne 引入的依赖版本。原来是公司父级定义了旧版本,导致引入的最新 SDK 版本内的 Kotlin 版本被覆盖了。于是在最外层 pom.xml 下重新定义了版本约束。之后就是开发的小细节问题了。

感觉 KubernetesSDK 包还是很厉害的。下次也不知道有没有机会搞个 WebShell 连下 Pod