用Java编写kooperator

用java编写kooperator

本教程专门针对具有 java 背景、想要学习如何快速编写第一个 kubernetes 运算符的开发人员。为什么是运营商?有以下几个优点:

  • 显着减少维护,节省击键次数
  • 弹性内置于您创建的任何系统中
  • 学习的乐趣,认真了解 kubernetes 的具体细节

我会尝试将理论限制在最低限度,并展示一个万无一失的食谱如何“烤蛋糕”。我选择 java 是因为它比较接近我的工作经验,而且说实话它比 go 更容易(但有些人可能不同意)。

让我们直接跳到它。

理论与背景

没有人喜欢阅读冗长的文档,但让我们快速了解一下,好吗?

什么是 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的详细内容,更多请关注其它相关文章!