使用Java编写第一个Kubernetes Operator的教程

发布:2024-10-17 15:58 阅读:67 点赞:0

本教程专为具有Java背景的开发人员设计,旨在帮助他们快速学习如何编写第一个Kubernetes Operator。为什么选择Operator?原因有以下几点:

  1. 显著减少维护工作,节省输入时间
  2. 系统内置韧性
  3. 学习的乐趣,深入了解Kubernetes的细节

我将尽量减少理论部分,提供一个简单易行的“制作蛋糕”方法。选择Java是因为它与我的工作经验较为接近,并且老实说,比Go更容易(尽管有些人可能不同意)。

让我们直接开始吧!

一. 理论与背景

没有人喜欢阅读冗长的文档,但我们快速过一下这个部分。

1. 什么是Pod?

Pod是一组具有共享网络接口(并且分配有唯一IP地址)和存储的容器。

2. 什么是Replica Set?

Replica Set控制Pod的创建与删除,以确保在任何时刻都有指定数量的Pod在运行。

3. 什么是Deployment?

Deployment拥有Replica Set,并间接拥有Pod。当你创建Deployment时,Pod会被创建;当你删除Deployment时,Pod会被删除。

4. 什么是Service?

Service是多个Pod的单一互联网端点(它均匀分配负载)。你可以将其暴露以便从集群外部可见。它自动化创建端点切片的过程。

Kubernetes自设计之初就被构建为无状态(stateless)的。Replica Set并不跟踪Pod的身份,当某个Pod消失时,会创建一个新的Pod。这导致某些需要状态的用例(例如数据库和缓存集群)难以实现。虽然Stateful Sets在某种程度上缓解了这个问题,但仍然不够。因此,人们开始编写Operator,以减轻维护负担。

5. 控制器与协调(Reconciliation)

Kubernetes中的一切运作都基于简单的控制循环概念。控制循环会检查某种资源的当前状态与期望状态(在清单中定义)的差异。如果存在差异,它会尝试执行某些操作来修复这个差异,这个过程称为协调。

Operator实际上是针对自定义资源的相同概念。自定义资源(Custom Resources)是扩展Kubernetes API的手段,你可以通过定义CRD(Custom Resource Definition)来实现对某些资源类型的管理。一旦在Kubernetes中设置了CRD,所有操作如获取、列出、更新、删除等都可以在此资源上进行。而实际执行这些操作的,就是我们的Operator。

二. 激励示例与Java应用程序

通常,在测试新技术时,我们会选择最基本的问题来解决。由于这个概念相对复杂,因此本示例的“Hello World”会稍微长一点。根据我看到的资料,最简单的用例是设置静态页面服务。

1. 项目目标

我们将定义一个自定义资源,表示我们要服务的两个页面。应用该资源后,Operator将自动设置Spring Boot应用程序、创建包含页面内容的ConfigMap、将ConfigMap挂载到应用的Pod中的一个卷上,并为该Pod设置Service。更有趣的是,如果我们修改资源,它会动态更新,新的页面更改将立即可见。如果我们删除资源,它将清理所有内容,使集群保持干净。

三. 服务Java应用程序

这个静态页面服务器在Spring Boot中非常简单。只需使用spring-boot-starter-web即可,因此可以前往Spring初始化器选择以下配置:

  • Maven
  • Java 21
  • 最新稳定版本(对我来说是3.3.4)
  • GraalVM
  • Spring Boot Starter Web

应用程序代码如下:

@SpringBootApplication
@RestController
public class WebpageServingApplication {

    // 处理对页面的请求
    @GetMapping(value = "/{page}", produces = "text/html")
    public String page(@PathVariable String page) throws IOException {
        // 从/static目录读取指定页面内容
        return Files.readString(Path.of("/static/" + page));
    }

    public static void main(String[] args) {
        // 启动Spring Boot应用程序
        SpringApplication.run(WebpageServingApplication.classargs);
    }
}

此处,我们将从/static目录获取路径变量所传递的页面内容(在我们的例子中是page1page2)。静态目录将从ConfigMap挂载,后面会详细介绍。

2. 构建原生镜像并推送到远程仓库

提示1

pom.xml中配置GraalVM如下,以获得最低内存消耗(大约2GB)和最快构建速度:

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
        <buildArgs>
            <buildArg>-Ob</buildArg>
        </buildArgs>
    </configuration>
</plugin>

由于我只有16GB的内存,并且安装了很多东西,这个配置对我来说是必须的。

提示2

pom.xml中配置Spring Boot Maven插件如下:

<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,你将无法附加到容器中。一旦完成,可以切换到其他版本。

publishtrue将导致构建的镜像推送到仓库,因此请切换为你的仓库。

BP_JVM_VERSION是构建镜像的Java版本,应与项目的Java版本相同。根据我所知,最新可用的Java版本是21。

现在你可以运行以下命令:

mvn spring-boot:build-image

这样就完成了镜像的构建。

四. 使用Fabric8编写Operator

现在,乐趣开始了。首先,在pom.xml中添加以下依赖:

<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并生成清单。

1. 定义自定义资源

以下是自定义资源的示例代码:

@Group("com.github.webserving")
@Version("v1alpha1")
@ShortNames("websrv")
public class WebServingResource extends CustomResource<WebServingSpecWebServingStatusimplements Namespaced {
}

// 定义WebServingSpec类,包含两个页面字段
public record WebServingSpec(String page1, String page2) {
}

// 定义WebServingStatus类,包含一个状态字段
public record WebServingStatus(String status) {
}

所有Kubernetes资源清单的共同之处在于它们通常都有specstatus。你可以看到,spec将包含以heredoc格式粘贴的两个页面。正确的做法是操作状态,以反映Operator正在执行的操作。例如,如果它在等待Deployment完成,则状态为“Processing”;一旦完成,它将状态更改为“Ready”。但我们将跳过这部分,因为这是一个简单的演示。

2. Operator的逻辑

Operator的逻辑主要在主类中,非常简洁。以下是逐步实现的过程:

// 创建Kubernetes客户端
KubernetesClient client = new KubernetesClientBuilder()
    .withTaskExecutor(executor).build();

// 获取CRD客户端
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