使用Java编写第一个Kubernetes Operator的教程
本教程专为具有Java背景的开发人员设计,旨在帮助他们快速学习如何编写第一个Kubernetes Operator。为什么选择Operator?原因有以下几点:
-
显著减少维护工作,节省输入时间 -
系统内置韧性 -
学习的乐趣,深入了解Kubernetes的细节
我将尽量减少理论部分,提供一个简单易行的“制作蛋糕”方法。选择Java是因为它与我的工作经验较为接近,并且老实说,比Go更容易(尽管有些人可能不同意)。
让我们直接开始吧!
一. 理论与背景
没有人喜欢阅读冗长的文档,但我们快速过一下这个部分。
1. 什么是Pod?
Pod是一组具有共享网络接口(并且分配有唯一IP地址)和存储的容器。
2. 什么是Replica Set?
Replica Set控制Pod的创建与删除,以确保在任何时刻都有指定数量的Pod在运行。
3. 什么是Deployment?
Deployment拥有Replica Set,并间接拥有Pod。当你创建Deployment时,Pod会被创建;当你删除Deployment时,Pod会被删除。
4. 什么是Service?
Service是多个Pod的单一互联网端点(它均匀分配负载)。你可以将其暴露以便从集群外部可见。它自动化创建端点切片的过程。
Kubernetes自设计之初就被构建为无状态(stateless)的。Replica Set并不跟踪Pod的身份,当某个Pod消失时,会创建一个新的Pod。这导致某些需要状态的用例(例如数据库和缓存集群)难以实现。虽然Stateful Sets在某种程度上缓解了这个问题,但仍然不够。因此,人们开始编写Operator,以减轻维护负担。
5. 控制器与协调(Reconciliation)
Kubernetes中的一切运作都基于简单的控制循环概念。控制循环会检查某种资源的当前状态与期望状态(在清单中定义)的差异。如果存在差异,它会尝试执行某些操作来修复这个差异,这个过程称为协调。
Operator实际上是针对自定义资源的相同概念。自定义资源(Custom Resources)是扩展Kubernetes API的手段,你可以通过定义CRD(Custom Resource Definition)来实现对某些资源类型的管理。一旦在Kubernetes中设置了CRD,所有操作如获取、列出、更新、删除等都可以在此资源上进行。而实际执行这些操作的,就是我们的Operator。
二. 激励示例与Java应用程序
通常,在测试新技术时,我们会选择最基本的问题来解决。由于这个概念相对复杂,因此本示例的“Hello World”会稍微长一点。根据我看到的资料,最简单的用例是设置静态页面服务。
1. 项目目标
我们将定义一个自定义资源,表示我们要服务的两个页面。应用该资源后,Operator将自动设置Spring Boot应用程序、创建包含页面内容的ConfigMap、将ConfigMap挂载到应用的Pod中的一个卷上,并为该Pod设置Service。更有趣的是,如果我们修改资源,它会动态更新,新的页面更改将立即可见。如果我们删除资源,它将清理所有内容,使集群保持干净。
三. 服务Java应用程序
这个静态页面服务器在Spring Boot中非常简单。只需使用spring-boot-starter-web
即可,因此可以前往Spring初始化器选择以下配置:
-
Maven -
Java 21 -
最新稳定版本(对我来说是3.3.4) -
GraalVM -
Spring Boot Starter Web
应用程序代码如下:
@SpringBootApplication
@RestController
public class WebpageServingApplication {
// 处理对页面的请求
@GetMapping(value = "/{page}", produces = "text/html")
public String page(@PathVariable String page) throws IOException {
// 从/static目录读取指定页面内容
return Files.readString(Path.of("/static/" + page));
}
public static void main(String[] args) {
// 启动Spring Boot应用程序
SpringApplication.run(WebpageServingApplication.class, args);
}
}
此处,我们将从/static
目录获取路径变量所传递的页面内容(在我们的例子中是page1
和page2
)。静态目录将从ConfigMap挂载,后面会详细介绍。
2. 构建原生镜像并推送到远程仓库
提示1
在pom.xml
中配置GraalVM如下,以获得最低内存消耗(大约2GB)和最快构建速度:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>-Ob</buildArg>
</buildArgs>
</configuration>
</plugin>
由于我只有16GB的内存,并且安装了很多东西,这个配置对我来说是必须的。
提示2
在pom.xml
中配置Spring Boot Maven插件如下:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<publish>true</publish>
<builder>paketobuildpacks/builder-jammy-full:latest</builder>
<name>ghcr.io/dgawlik/webpage-serving:1.0.5</name>
<env>
<BP_JVM_VERSION>21</BP_JVM_VERSION>
</env>
</image>
<docker>
<publishRegistry>
<url>https://ghcr.io/dgawlik</url>
<username>dgawlik</username>
<password>${env.GITHUB_TOKEN}</password>
</publishRegistry>
</docker>
</configuration>
</plugin>
在测试期间使用paketobuildpacks/builder-jammy-full:latest
,因为-tiny
和-base
不会安装bash,你将无法附加到容器中。一旦完成,可以切换到其他版本。
publish
为true
将导致构建的镜像推送到仓库,因此请切换为你的仓库。
BP_JVM_VERSION
是构建镜像的Java版本,应与项目的Java版本相同。根据我所知,最新可用的Java版本是21。
现在你可以运行以下命令:
mvn spring-boot:build-image
这样就完成了镜像的构建。
四. 使用Fabric8编写Operator
现在,乐趣开始了。首先,在pom.xml
中添加以下依赖:
<dependencies>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>6.13.4</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>crd-generator-apt</artifactId>
<version>6.13.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
crd-generator-apt
是一个插件,用于扫描项目,检测CRD POJO并生成清单。
1. 定义自定义资源
以下是自定义资源的示例代码:
@Group("com.github.webserving")
@Version("v1alpha1")
@ShortNames("websrv")
public class WebServingResource extends CustomResource<WebServingSpec, WebServingStatus> implements Namespaced {
}
// 定义WebServingSpec类,包含两个页面字段
public record WebServingSpec(String page1, String page2) {
}
// 定义WebServingStatus类,包含一个状态字段
public record WebServingStatus(String status) {
}
所有Kubernetes资源清单的共同之处在于它们通常都有spec
和status
。你可以看到,spec
将包含以heredoc格式粘贴的两个页面。正确的做法是操作状态,以反映Operator正在执行的操作。例如,如果它在等待Deployment完成,则状态为“Processing”;一旦完成,它将状态更改为“Ready”。但我们将跳过这部分,因为这是一个简单的演示。
2. Operator的逻辑
Operator的逻辑主要在主类中,非常简洁。以下是逐步实现的过程:
// 创建Kubernetes客户端
KubernetesClient client = new KubernetesClientBuilder()
.withTaskExecutor(executor).build();
// 获取CRD客户端
var crdClient = client.resources(WebServingResource.class)
.inNamespace("default");
// 创建通用资源事件处理程序
var handler = new GenericResourceEventHandler<>(update -> {
synchronized (changes) {
changes.notifyAll(); // 通知所有等待的线程
}
});
// 启动事件监听
crdClient.inform(handler).start();
client.apps().deployments().inNamespace("default")
.withName("web