用Java编写kooperator
2024-10-22 21:56:49
本教程专门针对具有 java 背景、想要学习如何快速编写第一个 kubernetes 运算符的开发人员。为什么是运营商?有以下几个优点:
- 显着减少维护,节省击键次数
- 弹性内置于您创建的任何系统中
- 学习的乐趣,认真了解 kubernetes 的具体细节
我会尝试将理论限制在最低限度,并展示一个万无一失的食谱如何“烤蛋糕”。我选择 java 是因为它比较接近我的工作经验,而且说实话它比 go 更容易(但有些人可能不同意)。
让我们直接跳到它。
理论与背景没有人喜欢阅读冗长的文档,但让我们快速了解一下,好吗?
立即学习“Java免费学习笔记(深入)”;
什么是 pod?
pod 是一组具有共享网络接口(并给定唯一的 ip 地址)和存储的容器。
什么是副本集?
副本集控制 pod 的创建和删除,以便在每个时刻都有指定模板数量的 pod。
什么是部署?
deployment 拥有副本集并间接拥有 pod。当您创建部署时,pod 就会被创建,当您删除它时,pod 就会消失。
什么是服务?
服务是一组 pod 的单一互联网端点(它在它们之间平均分配负载)。您可以将其公开为从集群外部可见。它自动创建端点切片。
kubernetes 的问题在于它从一开始就被设计为无状态的。副本集不会跟踪 pod 的身份,当特定 pod 消失时,就会创建新的 pod。有一些用例需要状态,例如数据库和缓存集群。有状态集只能部分缓解这个问题。
这就是为什么人们开始编写运算符来减轻维护负担的原因。我不会深入讨论该模式和各种 sdks — 您可以从这里开始。
控制器和调节kubernetes 中工作的一切、机器的每个微小齿轮都基于控制循环的简单概念。因此,此控制循环对于特定资源类型的作用是检查是什么以及应该是什么(如清单中所定义)。如果存在不匹配,它会尝试执行一些操作来修复该问题。这就是所谓的和解。
运算符的真正含义是相同的概念,但针对的是自定义资源。自定义资源是将 kubernetes api 扩展到您定义的某些资源类型的方法。如果您在 kubernetes 中设置了 crd,则可以在此资源上执行所有操作,例如获取、列出、更新、删除等。实际工作会做什么?没错——我们的运营商。
激励示例和 java 应用程序作为第一次测试技术的典型,您选择最基本的问题。因为概念特别复杂,所以本例中的 hello world 会有点长。无论如何,在大多数来源中,我看到最简单的用例是设置静态页面服务。
所以项目是这样的:我们将定义代表我们想要服务的两个页面的自定义资源。应用该资源后,操作员将自动在 spring boot 中设置服务应用程序,创建包含页面内容的配置映射,将配置映射装载到 apps pod 中的卷中,并为该 pod 设置服务。有趣的是,如果我们修改资源,它将动态重新绑定所有内容,并且新页面更改将立即可见。第二个有趣的事情是,如果我们删除资源,它将删除所有内容,使我们的集群保持干净。
提供 java 应用程序
这将是 spring boot 中非常简单的静态页面服务器。您只需要 spring-boot-starter-web,因此请继续使用 spring 初始化程序并选择:
- 行家
- java 21
- 最新稳定版本(对我来说是3.3.4)
- graal 虚拟机
- 和 spring boot 入门网站
应用程序就是这样:
@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>
- 在测试时使用 paketobuildpacks/builder-jammy-full:latest 因为 -tiny 和 -base 不会安装 bash,并且您将无法附加到容器。完成后即可切换。
- publish true 将导致构建镜像将其推送到存储库,因此请继续将其切换到您的存储库
- bp_jvm_version 将是构建器映像的 java 版本,它应该与您项目的 java 相同。据我所知,最新的 java 版本是 21。
所以现在你可以:
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 { } </webservingspec>
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); } } </t></t></t></t>
因为我们只需要在所有情况下唤醒循环,所以我们向它传递一个通用的 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的详细内容,更多请关注图灵教育其它相关文章!