当前位置: 首页 > 图灵资讯 > 技术篇> 用Java编写kooperator

用Java编写kooperator

来源:图灵教育
时间:2024-10-22 21:56:49

用java编写kooperator

本教程专门针对具有 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 -&gt; {
   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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (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的详细内容,更多请关注图灵教育其它相关文章!

上一篇:

Java 中的记录与类

下一篇:

返回列表