Maison > Java > javaDidacticiel > Écrire un opérateur en Java

Écrire un opérateur en Java

Linda Hamilton
Libérer: 2024-10-17 06:07:29
original
803 Les gens l'ont consulté

Writing koperator in Java

Ce tutoriel est spécifiquement destiné aux développeurs ayant une expérience Java qui souhaitent apprendre à écrire rapidement le premier opérateur Kubernetes. Pourquoi des opérateurs ? Il y a plusieurs avantages :

  • Réduction significative de la maintenance, économie de frappes
  • résilience intégrée au système que vous créez
  • le plaisir d'apprendre, prendre au sérieux les écrous et boulons de Kubernetes

Je vais essayer de limiter la théorie au minimum et de montrer une recette infaillible comment « faire un gâteau ». J'ai choisi Java car il est proche de mon expérience professionnelle et pour être honnête, c'est plus facile que Go (mais certains peuvent ne pas être d'accord).

Allons-y directement.

Théorie et contexte

Personne n’aime lire une longue documentation, mais sortons-le rapidement de notre poitrine, d’accord ?

Qu'est-ce qu'un pod ?

Pod est un groupe de conteneurs avec une interface réseau partagée (et une adresse IP unique) et un stockage.

Qu'est-ce qu'un ensemble de répliques ?

Le jeu de réplicas contrôle la création et la suppression de pods afin qu'à chaque instant, il y ait exactement un nombre spécifié de pods avec un modèle donné.

Qu'est-ce que le déploiement ?

Le déploiement possède un jeu de réplicas et possède indirectement des pods. Lorsque vous créez des pods de déploiement, des pods sont créés lorsque vous les supprimez.

Qu'est-ce que le service ?

Le service est un point de terminaison Internet UNIQUE pour un groupe de pods (il répartit la charge entre eux de manière égale). Vous pouvez l'exposer pour qu'il soit visible de l'extérieur du cluster. Il automatise la création de tranches de points de terminaison.

Le problème avec Kubernetes est que dès le départ, il a été conçu pour être apatride. Les ensembles de réplicas ne suivent pas l'identité des pods ; lorsqu'un pod particulier disparaît, un nouveau est simplement créé. Certains cas d'utilisation nécessitent un état, comme les bases de données et les clusters de cache. Les ensembles avec état n'atténuent que partiellement le problème.

C'est pourquoi les gens ont commencé à écrire des opérateurs pour alléger le fardeau de la maintenance. Je n'entrerai pas dans les profondeurs du modèle et des différents SDK — vous pouvez commencer à partir d'ici.

Contrôleurs et rapprochement

Tout ce qui fonctionne dans Kubernetes, chaque petit équipement de machinerie est basé sur un concept simple de boucle de contrôle. Ainsi, ce que fait cette boucle de contrôle pour un type de ressource particulier, c'est qu'elle vérifie ce qui est et ce qui devrait être (tel que défini dans le manifeste). S'il y a une incompatibilité, il essaie d'effectuer certaines actions pour résoudre ce problème. C'est ce qu'on appelle la réconciliation.

Et ce que sont réellement les opérateurs, c'est le même concept mais pour les ressources personnalisées. Les ressources personnalisées sont le moyen d'étendre l'API Kubernetes à certains types de ressources que vous définissez. Si vous configurez crd dans Kubernetes, toutes les actions telles que obtenir, lister, mettre à jour, supprimer, etc. seront possibles sur cette ressource. Et qu’est-ce qui fera le travail réel ? C'est vrai, notre opérateur.

Exemple motivant et application Java

Comme d'habitude pour tester une technologie pour la première fois, vous choisissez le problème le plus basique à résoudre. Parce que le concept est particulièrement complexe alors hello world dans ce cas sera un peu long. Quoi qu'il en soit, dans la plupart des sources, j'ai vu que le cas d'utilisation le plus simple consiste à configurer la diffusion de pages statiques.

Le projet est donc comme ceci : nous allons définir une ressource personnalisée qui représente deux pages que nous voulons servir. Après avoir appliqué cet opérateur de ressources, il configurera automatiquement l'application de service dans Spring Boot, créera une carte de configuration avec le contenu des pages, montera la carte de configuration dans un volume dans le pod d'applications et configurera le service pour ce pod. Ce qui est amusant, c'est que si nous modifions la ressource, elle reliera tout à la volée et les nouvelles modifications de page seront instantanément visibles. La deuxième chose amusante est que si nous supprimons la ressource, cela supprimera tout, laissant notre cluster propre.

Servir l'application Java

Ce sera un serveur de pages statiques très simple dans Spring Boot. Vous n'aurez besoin que de spring-boot-starter-web, alors allez-y et choisissez :

  • maven
  • Java 21
  • dernière version stable (3.3.4 pour moi)
  • graal vm
  • et le site Web de démarrage Spring Boot

L'application est juste ceci :

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

}
Copier après la connexion

Tout ce que nous transmettons comme variable de chemin sera récupéré du répertoire /static (dans notre cas, page1 et page2). Le répertoire statique sera donc monté à partir de la carte de configuration, mais nous en reparlerons plus tard.

Alors maintenant, nous devons créer une image native et la transférer vers le référentiel distant.

Conseil numéro 1

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
        <buildArgs>
            <buildArg>-Ob</buildArg>
        </buildArgs>
    </configuration>
</plugin>
Copier après la connexion

En configurant GraalVM de cette manière, vous obtiendrez la construction la plus rapide avec la consommation de mémoire la plus faible (environ 2 Go). Pour moi, c'était un must car je n'ai que 16 Go de mémoire et beaucoup de choses installées.

Astuce numéro 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>
Copier après la connexion
  • 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>
Copier après la connexion

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 {
}
Copier après la connexion
public record WebServingSpec(String page1, String page2) {
}
Copier après la connexion
public record WebServingStatus (String status) {
}
Copier après la connexion

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();
Copier après la connexion

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);
    }
}
Copier après la connexion

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;
    }
Copier après la connexion

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;
}
Copier après la connexion

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));
}
Copier après la connexion

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

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

Copier après la connexion

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();
}
Copier après la connexion

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

Copier après la connexion

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
Copier après la connexion

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>
Copier après la connexion

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal