Heim  >  Artikel  >  Java  >  Koperator in Java schreiben

Koperator in Java schreiben

Linda Hamilton
Linda HamiltonOriginal
2024-10-17 06:07:29774Durchsuche

Writing koperator in Java

Dieses Tutorial richtet sich speziell an Entwickler mit Java-Hintergrund, die lernen möchten, wie man schnell den ersten Kubernetes-Operator schreibt. Warum Betreiber? Es gibt mehrere Vorteile:

  • deutliche Wartungsreduzierung, Einsparung von Tastenanschlägen
  • In das von Ihnen erstellte System integrierte Ausfallsicherheit
  • Spaß am Lernen, sich ernsthaft mit den Grundlagen von Kubernetes auseinanderzusetzen

Ich werde versuchen, die Theorie auf ein Minimum zu beschränken und ein narrensicheres Rezept zum „Backen eines Kuchens“ zu zeigen. Ich habe mich für Java entschieden, weil es meiner Berufserfahrung nahe kommt und ehrlich gesagt einfacher ist als Go (aber einige mögen anderer Meinung sein).

Lass uns gleich loslegen.

Theorie und Hintergrund

Niemand liest gerne lange Dokumentationen, aber lassen Sie uns das schnell aus der Fassung bringen, oder?

Was ist ein Pod?

Pod ist eine Gruppe von Containern mit gemeinsamer Netzwerkschnittstelle (und eindeutiger IP-Adresse) und Speicher.

Was ist ein Replika-Set?

Der Replikatsatz steuert das Erstellen und Löschen von Pods, sodass zu jedem Zeitpunkt genau die angegebene Anzahl von Pods mit der angegebenen Vorlage vorhanden ist.

Was ist Bereitstellung?

Die Bereitstellung besitzt einen Replikatsatz und indirekt auch Pods. Wenn Sie Bereitstellungs-Pods erstellen, werden sie erstellt. Wenn Sie sie löschen, sind sie nicht mehr vorhanden.

Was ist Service?

Der Dienst ist EIN EINZIGER Internet-Endpunkt für eine Reihe von Pods (er verteilt die Last gleichmäßig auf sie). Sie können es so bereitstellen, dass es von außerhalb des Clusters sichtbar ist. Es automatisiert die Erstellung von Endpunkt-Slices.

Das Problem bei Kubernetes besteht darin, dass es von Anfang an so konzipiert war, dass es zustandslos ist. Replikatsätze verfolgen nicht die Identität von Pods. Wenn ein bestimmter Pod nicht mehr vorhanden ist, wird einfach ein neuer erstellt. Es gibt einige Anwendungsfälle, die einen Status benötigen, wie z. B. Datenbanken und Cache-Cluster. Stateful Sets mildern das Problem nur teilweise.

Aus diesem Grund begann man, Operatoren zu schreiben, um den Wartungsaufwand zu verringern. Ich werde nicht näher auf das Muster und verschiedene SDKS eingehen – Sie können hier beginnen.

Controller und Versöhnung

Alles, was in Kubernetes funktioniert, jedes kleine Maschinengetriebe basiert auf dem einfachen Konzept eines Regelkreises. Was dieser Regelkreis also für einen bestimmten Ressourcentyp bewirkt, ist, dass er prüft, was ist und was sein sollte (wie im Manifest definiert). Wenn es eine Nichtübereinstimmung gibt, versucht es, einige Maßnahmen durchzuführen, um das Problem zu beheben. Dies nennt man Versöhnung.

Und was Operatoren wirklich sind, ist das gleiche Konzept, außer für benutzerdefinierte Ressourcen. Benutzerdefinierte Ressourcen sind die Möglichkeit, die Kubernetes-API auf einige von Ihnen definierte Ressourcentypen zu erweitern. Wenn Sie crd in Kubernetes einrichten, sind auf dieser Ressource alle Aktionen wie Abrufen, Auflisten, Aktualisieren, Löschen usw. möglich. Und was wird die eigentliche Arbeit sein? Das ist richtig – unser Betreiber.

Motivierendes Beispiel und Java-App

Wie beim ersten Testen von Technologie üblich, wählen Sie das Problem aus, das am einfachsten zu lösen ist. Da das Konzept besonders komplex ist, wird „Hello World“ in diesem Fall etwas lang. Wie auch immer, in den meisten Quellen habe ich gesehen, dass der einfachste Anwendungsfall die Einrichtung einer statischen Seitenbereitstellung ist.

Das Projekt sieht also so aus: Wir definieren eine benutzerdefinierte Ressource, die zwei Seiten darstellt, die wir bereitstellen möchten. Nach der Anwendung dieser Ressource richtet der Operator automatisch die Bereitstellungsanwendung in Spring Boot ein, erstellt eine Konfigurationszuordnung mit Seiteninhalten, stellt die Konfigurationszuordnung in einem Volume im Apps-Pod bereit und richtet den Dienst für diesen Pod ein. Das Schöne daran ist, dass, wenn wir die Ressource ändern, alles im Handumdrehen neu gebunden wird und neue Seitenänderungen sofort sichtbar sind. Der zweite Spaß ist, dass, wenn wir die Ressource löschen, alles gelöscht wird und unser Cluster sauber bleibt.

Java-App bereitstellen

Dies wird ein wirklich einfacher statischer Seitenserver in Spring Boot sein. Sie benötigen nur Spring-Boot-Starter-Web. Gehen Sie also zum Spring-Initialisierer und wählen Sie Folgendes aus:

  • Maven
  • Java 21
  • neueste stabile Version (3.3.4 für mich)
  • graal vm
  • und Spring Boot Starter-Web

Die App ist genau das:

@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);
    }

}

Was auch immer wir als Pfadvariable übergeben, wird aus dem /static-Verzeichnis abgerufen (in unserem Fall Seite1 und Seite2). Das statische Verzeichnis wird also aus der Konfigurationszuordnung gemountet, aber dazu später.

Jetzt müssen wir also ein natives Image erstellen und es in das Remote-Repository übertragen.

Tipp Nummer 1

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

Konfigurieren Sie GraalVM so, dass Sie den schnellsten Build mit dem geringsten Speicherverbrauch (ca. 2 GB) erhalten. Für mich war es ein Muss, da ich nur 16 GB Speicher und viele Dinge installiert habe.

Tipp Nummer 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>
  • use paketobuildpacks/builder-jammy-full:latest while you are testing because -tiny and -base won’t have bash installed and you won’t be able to attach to container. Once you are done you can switch.
  • publish true will cause building image to push it to repository, so go ahead and switch it to your repo
  • BP_JVM_VERSION will be the java version of the builder image, it should be the same as the java of your project. As far as I know the latest java available is 21.

So now you do:

mvn spring-boot:build-image

And that’s it.

Operator with Fabric8

Now the fun starts. First you will need this in your 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 is a plugin that scans a project, detects CRD pojos and generates the manifest.

Since I mentioned it, these resources are:

@Group("com.github.webserving")
@Version("v1alpha1")
@ShortNames("websrv")
public class WebServingResource extends CustomResource<WebServingSpec, WebServingStatus> implements Namespaced {
}
public record WebServingSpec(String page1, String page2) {
}
public record WebServingStatus (String status) {
}

What is common in all resource manifests in Kubernetes is that most of them has spec and status. So you can see that the spec will consist of two pages pasted in heredoc format. Now, the proper way to handle things would be to manipulate status to reflect whatever operator is doing. If for example it is waiting on deployment to finish it would have status = “Processing”, on everything done it would patch the status to “Ready” and so on. But we will skip that because this is just simple demo.

Good news is that the logic of the operator is all in main class and really short. So step by step here it is:

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();

So the heart of the program is of course Fabric8 Kuberenetes client built in first line. It is convenient to customize it with own executor. I used famous virtual threads, so when waiting on blocking io java will suspend the logic and move to main.

How here is a new part. The most basic version would be to run forever the loop and put Thread.sleep(1000) in it or so. But there is more clever way - kubernetes informers. Informer is websocket connection to kubernetes api server and it informs the client each time the subscribed resource changes. There is more to it you can read on the internet for example how to use various caches which fetch updates all at once in batch. But here it just subscribes directly per resource. The handler is a little bit bloated so I wrote a helper class 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);
    }
}

Since we only need to wake up the loop in all of the cases then we pass it a generic lambda. The idea for the loop is to wait on lock in the end and then the informer callback releases the lock each time the changes are detected.

Next:

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;
    }

If there is no crd then there is nothing to be done. And if the crd changed then we have to reload everything.

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;
}

This is for the case that ConfigMap is changed in between the iterations. Since it is mounted in pod then we have to reload the deployment.

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));
}

If any of the service or deployment differs from the defaults we will replace them with the defaults.

synchronized (changes) {
    changes.wait();
}

Then the aforementioned lock.

So now the only thing is to define the desired configmap, service and deployment.

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();
}

The magic of the OwnerReference is that you mark the resource which is it’s parent. Whenever you delete the parent k8s will delete automatically all the dependant resources.

But you can’t run it yet. You need a docker credentials in kubernetes:

kubectl delete secret regcred

kubectl create secret docker-registry regcred \
  --docker-server=ghcr.io \
  --docker-username=dgawlik \
  --docker-password=$GITHUB_TOKEN

Run this script once. Then we also need to set up the ingress:

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

The workflow

So first you build the operator project. Then you take target/classes/META-INF/fabric8/webservingresources.com.github.webserving-v1.yml and apply it. From now on the kubernetes is ready to accept your crd. Here it is:

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>

You apply the crd kubectl apply -f src/main/resources/crd-instance.yaml. And then you run Main of the operator.

Then monitor the pod if it is up. Next just take the ip of the cluster:

minikube ip

And in your browser navigate to /page1 and /page2.

Then try to change the crd and apply it again. After a second you should see the changes.

The end.

Conclusion

A bright observer will notice that the code has some concurrency issues. A lot can happen in between the start and the end of the loop. But there are a lot of cases to consider and tried to keep it simple. You can do it as aftermath.

Like wise for the deployment. Instead of running it in IDE you can build the image the same way as for serving app and write deployment of it. That’s basically demystification of the operator — it is just a pod like every other.

I hope you found it useful.

Thanks for reading.

I almost forgot - here is the repo:

https://github.com/dgawlik/operator-hello-world

Das obige ist der detaillierte Inhalt vonKoperator in Java schreiben. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:Mittwochs-Links – Ausgabe 6Nächster Artikel:Mittwochs-Links – Ausgabe 6