0

0

用Java编写kooperator

碧海醫心

碧海醫心

发布时间:2024-10-17 14:21:38

|

494人浏览过

|

来源于dev.to

转载

用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


    org.graalvm.buildtools
    native-maven-plugin
    
        
            -ob
        
    

像这样配置 graalvm,您将以最低的内存消耗(大约 2gb)实现最快的构建。对我来说这是必须的,因为我只有 16gb 内存并且安装了很多东西。

提示2


    org.springframework.boot
    spring-boot-maven-plugin
    
        
            true
            paketobuildpacks/builder-jammy-full:latest
            ghcr.io/dgawlik/webpage-serving:1.0.5
            
                21
            
        
        
            
                https://ghcr.io/dgawlik
                dgawlik
                ${env.github_token}
            
        
    

  • 在测试时使用 paketobuildpacks/builder-jammy-full:latest 因为 -tiny 和 -base 不会安装 bash,并且您将无法附加到容器。完成后即可切换。
  • publish true 将导致构建镜像将其推送到存储库,因此请继续将其切换到您的存储库
  • bp_jvm_version 将是构建器映像的 java 版本,它应该与您项目的 java 相同。据我所知,最新的 java 版本是 21。

所以现在你可以:

mvn spring-boot:build-image

就是这样。

使用 fabric8 的运算符

现在乐趣开始了。首先,你的 pom 中需要这个:


   
      io.fabric8
      kubernetes-client
      6.13.4
   
   
      io.fabric8
      crd-generator-apt
      6.13.4
      provided
   

crd-generator-apt 是一个扫描项目、检测 crd pojo 并生成清单的插件。

Vozo
Vozo

Vozo是一款强大的AI视频编辑工具,可以帮助用户轻松重写、配音和编辑视频。

下载

既然我提到了,这些资源是:

@group("com.github.webserving")
@version("v1alpha1")
@shortnames("websrv")
public class webservingresource extends customresource 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 implements resourceeventhandler {

    private final consumer handler;

    public genericresourceeventhandler(consumer 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: |
    

Hola amigos!

Buenos dias!

page2: |

Hello my friend

Good evening

您应用 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

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

116

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

43

2026.01.26

spring boot框架优点
spring boot框架优点

spring boot框架的优点有简化配置、快速开发、内嵌服务器、微服务支持、自动化测试和生态系统支持。本专题为大家提供spring boot相关的文章、下载、课程内容,供大家免费下载体验。

135

2023.09.05

spring框架有哪些
spring框架有哪些

spring框架有Spring Core、Spring MVC、Spring Data、Spring Security、Spring AOP和Spring Boot。详细介绍:1、Spring Core,通过将对象的创建和依赖关系的管理交给容器来实现,从而降低了组件之间的耦合度;2、Spring MVC,提供基于模型-视图-控制器的架构,用于开发灵活和可扩展的Web应用程序等。

390

2023.10.12

Java Spring Boot开发
Java Spring Boot开发

本专题围绕 Java 主流开发框架 Spring Boot 展开,系统讲解依赖注入、配置管理、数据访问、RESTful API、微服务架构与安全认证等核心知识,并通过电商平台、博客系统与企业管理系统等项目实战,帮助学员掌握使用 Spring Boot 快速开发高效、稳定的企业级应用。

70

2025.08.19

Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性
Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性

Spring Boot 是一个基于 Spring 框架的 Java 开发框架,它通过 约定优于配置的原则,大幅简化了 Spring 应用的初始搭建、配置和开发过程,让开发者可以快速构建独立的、生产级别的 Spring 应用,无需繁琐的样板配置,通常集成嵌入式服务器(如 Tomcat),提供“开箱即用”的体验,是构建微服务和 Web 应用的流行工具。

36

2025.12.22

Java Spring Boot 微服务实战
Java Spring Boot 微服务实战

本专题深入讲解 Java Spring Boot 在微服务架构中的应用,内容涵盖服务注册与发现、REST API开发、配置中心、负载均衡、熔断与限流、日志与监控。通过实际项目案例(如电商订单系统),帮助开发者掌握 从单体应用迁移到高可用微服务系统的完整流程与实战能力。

184

2025.12.24

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1503

2023.10.24

2026赚钱平台入口大全
2026赚钱平台入口大全

2026年最新赚钱平台入口汇总,涵盖任务众包、内容创作、电商运营、技能变现等多类正规渠道,助你轻松开启副业增收之路。阅读专题下面的文章了解更多详细内容。

54

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Git 教程
Git 教程

共21课时 | 3.2万人学习

Git版本控制工具
Git版本控制工具

共8课时 | 1.5万人学习

Git中文开发手册
Git中文开发手册

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号