
本教程专门针对具有 java 背景、想要学习如何快速编写第一个 kubernetes 运算符的开发人员。为什么是运营商?有以下几个优点:
我会尝试将理论限制在最低限度,并展示一个万无一失的食谱如何“烤蛋糕”。我选择 java 是因为它比较接近我的工作经验,而且说实话它比 go 更容易(但有些人可能不同意)。
让我们直接跳到它。
没有人喜欢阅读冗长的文档,但让我们快速了解一下,好吗?
立即学习“Java免费学习笔记(深入)”;
什么是 pod?
pod 是一组具有共享网络接口(并给定唯一的 ip 地址)和存储的容器。
什么是副本集?
副本集控制 pod 的创建和删除,以便在每个时刻都有指定模板数量的 pod。
什么是部署?
deployment 拥有副本集并间接拥有 pod。当您创建部署时,pod 就会被创建,当您删除它时,pod 就会消失。
什么是服务?
服务是一组 pod 的单一互联网端点(它在它们之间平均分配负载)。您可以将其公开为从集群外部可见。它自动创建端点切片。
kubernetes 的问题在于它从一开始就被设计为无状态的。副本集不会跟踪 pod 的身份,当特定 pod 消失时,就会创建新的 pod。有一些用例需要状态,例如数据库和缓存集群。有状态集只能部分缓解这个问题。
这就是为什么人们开始编写运算符来减轻维护负担的原因。我不会深入讨论该模式和各种 sdks — 您可以从这里开始。
kubernetes 中工作的一切、机器的每个微小齿轮都基于控制循环的简单概念。因此,此控制循环对于特定资源类型的作用是检查是什么以及应该是什么(如清单中所定义)。如果存在不匹配,它会尝试执行一些操作来修复该问题。这就是所谓的和解。
运算符的真正含义是相同的概念,但针对的是自定义资源。自定义资源是将 kubernetes api 扩展到您定义的某些资源类型的方法。如果您在 kubernetes 中设置了 crd,则可以在此资源上执行所有操作,例如获取、列出、更新、删除等。实际工作会做什么?没错——我们的运营商。
作为第一次测试技术的典型,您选择最基本的问题。因为概念特别复杂,所以本例中的 hello world 会有点长。无论如何,在大多数来源中,我看到最简单的用例是设置静态页面服务。
所以项目是这样的:我们将定义代表我们想要服务的两个页面的自定义资源。应用该资源后,操作员将自动在 spring boot 中设置服务应用程序,创建包含页面内容的配置映射,将配置映射装载到 apps pod 中的卷中,并为该 pod 设置服务。有趣的是,如果我们修改资源,它将动态重新绑定所有内容,并且新页面更改将立即可见。第二个有趣的事情是,如果我们删除资源,它将删除所有内容,使我们的集群保持干净。
提供 java 应用程序
这将是 spring boot 中非常简单的静态页面服务器。您只需要 spring-boot-starter-web,因此请继续使用 spring 初始化程序并选择:
应用程序就是这样:
@springbootapplication
@restcontroller
public class webpageservingapplication {
@getmapping(value = "/{page}", produces = "text/html")
public string page(@pathvariable string page) throws ioexception {
return files.readstring(path.of("/static/"+page));
}
public static void main(string[] args) {
springapplication.run(webpageservingapplication.class, args);
}
}
无论我们作为路径变量传递什么,都将从 /static 目录中获取(在我们的例子中为 page1 和 page2)。因此静态目录将从配置映射中挂载,但稍后再说。
所以现在我们必须构建一个原生镜像并将其推送到远程存储库。
提示1
<plugin>
<groupid>org.graalvm.buildtools</groupid>
<artifactid>native-maven-plugin</artifactid>
<configuration>
<buildargs>
<buildarg>-ob</buildarg>
</buildargs>
</configuration>
</plugin>
像这样配置 graalvm,您将以最低的内存消耗(大约 2gb)实现最快的构建。对我来说这是必须的,因为我只有 16gb 内存并且安装了很多东西。
提示2
<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>
所以现在你可以:
mvn spring-boot:build-image
就是这样。
使用 fabric8 的运算符
现在乐趣开始了。首先,你的 pom 中需要这个:
<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 并生成清单的插件。
既然我提到了,这些资源是:
@group("com.github.webserving")
@version("v1alpha1")
@shortnames("websrv")
public class webservingresource extends customresource<webservingspec, webservingstatus> implements namespaced {
}
public record webservingspec(string page1, string page2) {
}
public record webservingstatus (string status) {
}
kubernetes 中所有资源清单的共同点是,大多数资源清单都有规格和状态。因此,您可以看到该规范将由以heredoc 格式粘贴的两个页面组成。现在,处理事情的正确方法是操纵状态来反映操作员正在做的事情。例如,如果它正在等待部署完成,它将具有状态=“处理中”,完成所有操作后,它会将状态修补为“就绪”等。但我们将跳过它,因为这只是简单的演示。
好消息是运算符的逻辑全部在主类中并且非常短。所以一步一步来:
kubernetesclient client = new kubernetesclientbuilder()
.withtaskexecutor(executor).build();
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-serving-app-deployment").inform(handler).start();
client.services().innamespace("default")
.withname("web-serving-app-svc").inform(handler).start();
client.configmaps().innamespace("default")
.withname("web-serving-app-config").inform(handler).start();
所以该程序的核心当然是第一行内置的 fabric8 kuberenetes 客户端。使用自己的执行器进行定制很方便。我使用了著名的虚拟线程,因此当等待阻塞 io java 时,它将挂起逻辑并移至 main。
这是一个新部分。最基本的版本是永远运行循环并将 thread.sleep(1000) 放入其中。但还有更聪明的方法——kubernetes informers。 informer 是与 kubernetes api 服务器的 websocket 连接,每次订阅的资源发生变化时它都会通知客户端。您可以在互联网上阅读更多内容,例如如何使用各种缓存来批量获取所有更新。但在这里它只是直接订阅每个资源。该处理程序有点臃肿,所以我编写了一个辅助类 genericresourceeventhandler。
public class genericresourceeventhandler<t> implements resourceeventhandler<t> {
private final consumer<t> handler;
public genericresourceeventhandler(consumer<t> handler) {
this.handler = handler;
}
@override
public void onadd(t obj) {
this.handler.accept(obj);
}
@override
public void onupdate(t oldobj, t newobj) {
this.handler.accept(newobj);
}
@override
public void ondelete(t obj, boolean deletedfinalstateunknown) {
this.handler.accept(null);
}
}
因为我们只需要在所有情况下唤醒循环,所以我们向它传递一个通用的 lambda。循环的想法是最后等待锁定,然后通知者回调在每次检测到更改时释放锁定。
下一个:
for (; ; ) {
var crdlist = crdclient.list().getitems();
var crd = optional.ofnullable(crdlist.isempty() ? null : crdlist.get(0));
var skipupdate = false;
var reload = false;
if (!crd.ispresent()) {
system.out.println("no webservingresource found, reconciling disabled");
currentcrd = null;
skipupdate = true;
} else if (!crd.get().getspec().equals(
optional.ofnullable(currentcrd)
.map(webservingresource::getspec).orelse(null))) {
currentcrd = crd.orelse(null);
system.out.println("crd changed, reconciling configmap");
reload = true;
}
如果没有 crd 则无事可做。如果 crd 发生变化,那么我们必须重新加载所有内容。
var currentconfigmap = client.configmaps().innamespace("default")
.withname("web-serving-app-config").get();
if(!skipupdate && (reload || desiredconfigmap(currentcrd).equals(currentconfigmap))) {
system.out.println("new configmap, reconciling webservingresource");
client.configmaps().innamespace("default").withname("web-serving-app-config")
.createorreplace(desiredconfigmap(currentcrd));
reload = true;
}
这是针对 configmap 在迭代之间发生更改的情况。由于它已安装在 pod 中,因此我们必须重新加载部署。
var currentservingdeploymentnullable = client.apps().deployments().innamespace("default")
.withname("web-serving-app-deployment").get();
var currentservingdeployment = optional.ofnullable(currentservingdeploymentnullable);
if(!skipupdate && (reload || !desiredwebservingdeployment(currentcrd).getspec().equals(
currentservingdeployment.map(deployment::getspec).orelse(null)))) {
system.out.println("reconciling deployment");
client.apps().deployments().innamespace("default").withname("web-serving-app-deployment")
.createorreplace(desiredwebservingdeployment(currentcrd));
}
var currentservingservicenullable = client.services().innamespace("default")
.withname("web-serving-app-svc").get();
var currentservingservice = optional.ofnullable(currentservingservicenullable);
if(!skipupdate && (reload || !desiredwebservingservice(currentcrd).getspec().equals(
currentservingservice.map(service::getspec).orelse(null)))) {
system.out.println("reconciling service");
client.services().innamespace("default").withname("web-serving-app-svc")
.createorreplace(desiredwebservingservice(currentcrd));
}
如果任何服务或部署与默认值不同,我们会将其替换为默认值。
synchronized (changes) {
changes.wait();
}
然后是前面提到的锁。
所以现在唯一的事情就是定义所需的配置映射、服务和部署。
private static deployment desiredwebservingdeployment(webservingresource crd) {
return new deploymentbuilder()
.withnewmetadata()
.withname("web-serving-app-deployment")
.withnamespace("default")
.addtolabels("app", "web-serving-app")
.withownerreferences(createownerreference(crd))
.endmetadata()
.withnewspec()
.withreplicas(1)
.withnewselector()
.addtomatchlabels("app", "web-serving-app")
.endselector()
.withnewtemplate()
.withnewmetadata()
.addtolabels("app", "web-serving-app")
.endmetadata()
.withnewspec()
.addnewcontainer()
.withname("web-serving-app-container")
.withimage("ghcr.io/dgawlik/webpage-serving:1.0.5")
.withvolumemounts(new volumemountbuilder()
.withname("web-serving-app-config")
.withmountpath("/static")
.build())
.addnewport()
.withcontainerport(8080)
.endport()
.endcontainer()
.withvolumes(new volumebuilder()
.withname("web-serving-app-config")
.withconfigmap(new configmapvolumesourcebuilder()
.withname("web-serving-app-config")
.build())
.build())
.withimagepullsecrets(new localobjectreferencebuilder()
.withname("regcred").build())
.endspec()
.endtemplate()
.endspec()
.build();
}
private static service desiredwebservingservice(webservingresource crd) {
return new servicebuilder()
.editmetadata()
.withname("web-serving-app-svc")
.withownerreferences(createownerreference(crd))
.withnamespace(crd.getmetadata().getnamespace())
.endmetadata()
.editspec()
.addnewport()
.withport(8080)
.withtargetport(new intorstring(8080))
.endport()
.addtoselector("app", "web-serving-app")
.endspec()
.build();
}
private static configmap desiredconfigmap(webservingresource crd) {
return new configmapbuilder()
.withmetadata(
new objectmetabuilder()
.withname("web-serving-app-config")
.withnamespace(crd.getmetadata().getnamespace())
.withownerreferences(createownerreference(crd))
.build())
.withdata(map.of("page1", crd.getspec().page1(),
"page2", crd.getspec().page2()))
.build();
}
private static ownerreference createownerreference(webservingresource crd) {
return new ownerreferencebuilder()
.withapiversion(crd.getapiversion())
.withkind(crd.getkind())
.withname(crd.getmetadata().getname())
.withuid(crd.getmetadata().getuid())
.withcontroller(true)
.build();
}
ownerreference 的神奇之处在于您可以标记作为其父级的资源。每当您删除父 k8s 时,都会自动删除所有依赖资源。
但是你还不能运行它。您需要 kubernetes 中的 docker 凭据:
kubectl delete secret regcred kubectl create secret docker-registry regcred \ --docker-server=ghcr.io \ --docker-username=dgawlik \ --docker-password=$github_token
运行此脚本一次。然后我们还需要设置入口:
apiversion: networking.k8s.io/v1
kind: ingress
metadata:
name: demo-ingress
spec:
rules:
- http:
paths:
- path: /
pathtype: prefix
backend:
service:
name: web-serving-app-svc
port:
number: 8080
工作流程
因此,首先构建运算符项目。然后,您获取 target/classes/meta-inf/fabric8/webservingresources.com.github.webserving-v1.yml 并应用它。从现在开始,kubernetes 已准备好接受您的 crd。这是:
apiVersion: com.github.webserving/v1alpha1
kind: WebServingResource
metadata:
name: example-ws
namespace: default
spec:
page1: |
<h1>Hola amigos!</h1>
<p>Buenos dias!</p>
page2: |
<h1>Hello my friend</h1>
<p>Good evening</p>
您应用 crd kubectl apply -f src/main/resources/crd-instance.yaml。然后运行算子的 main。
然后监视 pod 是否已启动。接下来只需获取集群的 ip:
minikube ip
然后在浏览器中导航至 /page1 和 /page2。
然后尝试更改crd并再次应用。一秒钟后您应该会看到变化。
结束。
聪明的观察者会注意到代码存在一些并发问题。在循环的开始和结束之间可能会发生很多事情。但有很多情况需要考虑并尽量保持简单。你可以把它作为善后处理。
部署也是如此。您可以按照与服务应用程序相同的方式构建映像并编写其部署,而不是在 ide 中运行它。这基本上是对操作员的揭秘——它只是一个像其他 pod 一样的 pod。
希望您觉得它有用。
感谢您的阅读。
我差点忘了 - 这是仓库:
https://github.com/dgawlik/operator-hello-world
以上就是用Java编写kooperator的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号