Kubernetes是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful), Kubernetes提供了应用部署,规划,更新,维护的一种机制。

控制台kubectl命令可以帮助我们操作K8s部署Deployment、pod、Service等,但当遇到批量部署的情况,以及实现自动化云端服务的时候,如何在后端调用接口则显得尤为重要。

依赖

引入Maven依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>9.0.0</version>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-api</artifactId>
<version>9.0.0</version>
</dependency>

创建容器

image-20200907210934572

根据官方更新日志,9.0.0+ 版本以上更新了GenericKubernetesApi,但是经过我的使用发现,该接口在创建容器失败后,没有合适的对象封装异常,我们无法得知容器创建失败的原因,经过在github询问贡献者得知:

image-20200907211507320

根据现有版本的设计,需要使用V1Status接收返回的对象排查错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
Call call = callBuilder.build();
call = tweakCallForCoreV1Group(call);
JsonElement element = apiClient.<JsonElement>execute(call, JsonElement.class).getData();
return getKubernetesApiResponse(dataClass, element, apiClient.getJSON().getGson());
} catch (ApiException e) {
if (e.getCause() instanceof IOException) {
throw new IllegalStateException(e.getCause()); // make this a checked exception?
}
final V1Status status;
try {
status = apiClient.getJSON().deserialize(e.getResponseBody(), V1Status.class);
} catch (JsonSyntaxException jsonEx) {
throw new RuntimeException(jsonEx);
}
if (null == status) { // the response body can be something unexpected sometimes..
throw new RuntimeException(e);
}
return new KubernetesApiResponse<>(status, e.getCode());
}

以下出于使用方便考虑,依旧采用旧版本的接口调用K8s。

通过K8s接口创建容器大致分三步:

1. 创建与K8s api-server的连接;
 2. 构造容器接口;
 3. 构造容器对象;
 4. 调用容器接口创建容器。

创建Deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
   ApiClient apiClient = new ApiClient();

//配置K8s ApiServer地址
apiClient.setBasePath("http://localhost:8080");
Configuration.setDefaultApiClient(this.apiClient);

//配置Deployment接口
private AppsV1Api appsV1Api;
appsV1Api = new AppsV1Api(this.apiClient);

//配置标签
Map<String, String> matchLabels = new HashMap<>();
matchLabels.put("app", podName);
matchLabels.put("env", env);

//配置端口
List<V1ContainerPort> portList = new ArrayList<>();
V1ContainerPort port = new V1ContainerPort();
port.setName("httpd");
port.setContainerPort(80);
portList.add(port);

List<V1EnvVar> envs = new LinkedList<>();
V1EnvVar clientIdEnv = new V1EnvVar();
clientIdEnv.setName("client_id");
clientIdEnv.setValue(clientId);
envs.add(clientIdEnv);
V1EnvVar clientSecretEnv = new V1EnvVar();
clientSecretEnv.setName("client_secret");
clientSecretEnv.setValue(clientSecret);
envs.add(clientSecretEnv);
V1EnvVar redirectEnv = new V1EnvVar();
redirectEnv.setName("redirect_uri");
redirectEnv.setValue(redirectUri);
envs.add(redirectEnv);

//使用对象封装Deployment
V1Deployment deploy =
new V1DeploymentBuilder()
.withApiVersion("apps/v1")
.withKind("Deployment")
.withNewMetadata()
.withName(deploymentName)
.withNamespace("default")
.endMetadata()
.withNewSpec()
.withReplicas(1)
.withNewSelector()
.withMatchLabels(matchLabels)
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(matchLabels)
.endMetadata()
.withNewSpec()
.withContainers(
new V1Container()
.name(podName)
.image("cgwire")
.imagePullPolicy("Never")
.ports(portList)
.env(envs))
.endSpec()
.endTemplate()
.endSpec().build();
try {
//调用容器接口创建Deployment
appsV1Api.createNamespacedDeployment(
"default", //namespace
deploy, //Deployment对象
"true", //pretty
null, //dryRun
null); //fieldManager
} catch (ApiException e) {
throw new FastRuntimeException(BizCode.SERVICE_UNAVAILABLE.getCode(),e.getResponseBody());
}

创建Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
   ApiClient apiClient = new ApiClient();

//配置K8s ApiServer地址
apiClient.setBasePath("http://localhost:8080");
Configuration.setDefaultApiClient(this.apiClient);

//配置Service接口
CoreV1Api coreV1Api = new CoreV1Api(apiClient);

//构造Service对象
Map<String, String> map = new HashMap<>();
map.put("app", podName);
map.put("env", env);
V1Service svc =
new V1ServiceBuilder()
.withApiVersion("v1")
.withKind("Service")
.withNewMetadata()
.withName(serviceName)
.withNamespace("default")
.endMetadata()
.withNewSpec()
.withSelector(map)
.addNewPort()
.withName("http")
.withPort(80)
.withTargetPort(new IntOrString(80))
.endPort()
.endSpec()
.build();

try {
//调用接口操作K8s创建Service
coreV1Api.createNamespacedService(
svc.getMetadata().getNamespace(), //namespace
svc, //Service对象
"true", //pretty
null, //dryRun
null //fieldManager
);
}
catch (ApiException e) {
throw new FastRuntimeException(BizCode.SERVICE_UNAVAILABLE.getCode(),e.getResponseBody());
}
}

创建Ingress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
   ApiClient apiClient = new ApiClient();

//配置K8s ApiServer地址
apiClient.setBasePath("http://localhost:8080");
Configuration.setDefaultApiClient(this.apiClient);

//配置Ingress接口
ExtensionV1beta1Api extensionsV1beta1Api = new ExtensionsV1beta1Api(this.apiClient);

//构造Ingress对象
Map<String, String> annotations = new HashMap<>();
annotations.put("kubernetes.io/ingress.class", "nginx");

List<ExtensionsV1beta1HTTPIngressPath> ingressPaths = new ArrayList<>();
ExtensionsV1beta1HTTPIngressPath path =
new ExtensionsV1beta1HTTPIngressPath()
.path("/")
.backend(
new ExtensionsV1beta1IngressBackend()
.serviceName(serviceName)
.servicePort(new IntOrString(80)));
ingressPaths.add(path);

ExtensionsV1beta1Ingress ingress =
new ExtensionsV1beta1IngressBuilder()
.withApiVersion("extensions/v1beta1")
.withKind("Ingress")
.withNewMetadata()
.withName("ingress-" + subDomain)
.withNamespace("default")
.withAnnotations(annotations)
.endMetadata()
.withNewSpec()
.withRules(
new ExtensionsV1beta1IngressRule()
.host(subDomain+".cgclass.net")
.http(
new ExtensionsV1beta1HTTPIngressRuleValue()
.paths(ingressPaths)))
.endSpec()
.build();
try {
//调用容器接口创建Ingress
extensionsV1beta1Api.createNamespacedIngress(
ingress.getMetadata().getNamespace(), //namespace
ingress, //Ingress对象
"true", //pretty
null, //dryRun
null //fieldManager
);
}
catch (ApiException e) {
throw new FastRuntimeException(BizCode.SERVICE_UNAVAILABLE.getCode(),e.getResponseBody());
}

异常处理

该版本的容器接口会抛出封装好的异常对象ApiException,通过捕获ApiException我们可以找到容器创建失败的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
extensionsV1beta1Api.createNamespacedIngress(
ingress.getMetadata().getNamespace(),
ingress,
"true",
null,
null
);
}
catch (ApiException e) {
System.out.println(e.getCause());
System.out.println(e.getMessage());
System.out.println(e.getResponseBody());
System.out.println(e.getStackTrace());
System.out.println(e.getCode());
System.out.println(e.getResponseHeaders());
System.out.println(e.getLocalizedMessage());
System.out.println(e.getSuppressed());
throw new FastRuntimeException(BizCode.SERVICE_UNAVAILABLE.getCode(),e.getResponseBody());
}

总结

创建容器的流程看似复杂,但是思路十分清晰,先通过ApiClient创建与K8s的连接,然后初始化与该容器相匹配的容器接口,如AppsV1Api , CoreV1ApiExtensionsV1beta1Api ,将容器信息封装到对象V1Deployment , V1Service , ExtensionsV1beta1Ingress 中,最后调用容器接口创建容器并处理异常信息。

kubernetes-client/java 目前网上教程内容较少并且质量堪忧(CSDN最近复制粘贴风气堪忧),只能借助于官方文档与实例,所以我将我的理解分享给大家,如有更好的解决方案,也欢迎大家与我交流。

Talk is cheap, show me the code.