Rumah > Java > javaTutorial > teks badan

Menulis koperator di Jawa

Linda Hamilton
Lepaskan: 2024-10-17 06:07:29
asal
694 orang telah melayarinya

Writing koperator in Java

Tutorial ini khusus untuk pembangun dengan latar belakang Java yang ingin belajar cara menulis operator kubernetes pertama dengan pantas. Mengapa pengendali? Terdapat beberapa kelebihan:

  • pengurangan penyelenggaraan yang ketara, menjimatkan ketukan kekunci
  • ketahanan terbina dalam apa jua sistem yang anda cipta
  • seronok belajar, serius tentang kacang dan bolt kubernetes

Saya akan cuba mengehadkan teori kepada minimum dan menunjukkan resipi kalis bodoh cara "membakar kek". Saya memilih Java kerana ia hampir dengan pengalaman bekerja saya dan sejujurnya ia lebih mudah daripada Go (tetapi ada yang mungkin tidak bersetuju).

Mari melompat terus ke sana.

Teori dan latar belakang

Tiada siapa yang suka membaca dokumentasi yang panjang, tetapi mari kita dapatkan ini dengan cepat dari dada kita, boleh?

Apakah pod?

Pod ialah sekumpulan bekas dengan antara muka rangkaian kongsi (dan diberi alamat IP unik) serta storan.

Apakah set replika?

Set replika mengawal penciptaan dan pemadaman pod supaya pada setiap saat terdapat bilangan pod yang ditentukan dengan templat yang diberikan.

Apakah itu penempatan?

Deployment memiliki set replika dan secara tidak langsung memiliki pod. Apabila anda membuat pod penempatan dibuat, apabila anda memadamkannya pod akan hilang.

Apakah perkhidmatan itu?

Perkhidmatan ialah SATU titik akhir internet untuk sekumpulan pod (ia mengagihkan beban di antara mereka secara sama rata). Anda boleh mendedahkannya untuk kelihatan dari luar kelompok. Ia mengautomasikan penciptaan kepingan titik akhir.

Masalah dengan kubernetes ialah sejak dari awal ia direka bentuk tanpa kewarganegaraan. Set replika tidak menjejaki identiti pod, apabila pod tertentu hilang, pod baharu baru dibuat. Terdapat beberapa kes penggunaan yang memerlukan keadaan seperti pangkalan data dan kelompok cache. Set stateful hanya sebahagiannya mengurangkan masalah.

Inilah sebabnya orang mula menulis operator untuk mengurangkan beban penyelenggaraan. Saya tidak akan pergi ke kedalaman corak dan pelbagai sdk — anda boleh bermula dari sini.

Pengawal dan rekonsiliasi

Semua yang berfungsi dalam kubernetes, setiap gear kecil jentera adalah berdasarkan konsep mudah gelung kawalan. Jadi apa yang dilakukan oleh gelung kawalan ini untuk jenis sumber tertentu ialah ia menyemak apa yang ada dan apa yang sepatutnya (seperti yang ditakrifkan dalam manifes). Jika terdapat ketidakpadanan, ia cuba melakukan beberapa tindakan untuk membetulkannya. Ini dipanggil perdamaian.

Dan apakah pengendali sebenarnya adalah konsep yang sama tetapi untuk sumber tersuai. Sumber tersuai ialah cara untuk melanjutkan api kubernetes kepada beberapa jenis sumber yang ditakrifkan oleh anda. Jika anda menyediakan crd dalam kubernetes maka semua tindakan seperti dapatkan, senarai, kemas kini, padam dan sebagainya akan dapat dilakukan pada sumber ini. Dan apa yang akan melakukan kerja sebenar? Betul — pengendali kami.

Contoh motivasi dan aplikasi java

Seperti biasa untuk menguji teknologi buat kali pertama anda memilih masalah yang paling asas untuk dilakukan. Oleh kerana konsepnya sangat kompleks maka hello world dalam kes ini akan menjadi sedikit panjang. Bagaimanapun, dalam kebanyakan sumber saya telah melihat bahawa kes penggunaan yang paling mudah ialah menyediakan siaran halaman statik.

Jadi projeknya adalah seperti ini : kami akan mentakrifkan sumber tersuai yang mewakili dua halaman yang ingin kami sediakan. Selepas menggunakan operator sumber itu secara automatik akan menyediakan aplikasi penyajian dalam Spring Boot, buat peta konfigurasi dengan kandungan halaman, lekapkan peta konfigurasi ke dalam volum dalam pod aplikasi dan sediakan perkhidmatan untuk pod itu. Apa yang menyeronokkan tentang ini ialah jika kita mengubah suai sumber, ia akan mengikat semula segala-galanya dengan cepat dan perubahan halaman baharu akan kelihatan serta-merta. Perkara kedua yang menyeronokkan ialah jika kami memadamkan sumber, ia akan memadamkan segala-galanya menjadikan kluster kami bersih.

Menyajikan apl java

Ini akan menjadi pelayan halaman statik yang sangat mudah dalam Spring Boot. Anda hanya memerlukan spring-boot-starter-web jadi teruskan ke spring initializer dan pilih:

  • maven
  • java 21
  • versi stabil terbaharu (3.3.4 untuk saya)
  • graal vm
  • dan web pemula but spring

Apl ini hanyalah:

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

}
Salin selepas log masuk

Apa sahaja yang kami lalui sebagai pembolehubah laluan akan diambil daripada direktori /statik (dalam kes kami halaman1 dan halaman2). Jadi direktori statik akan dipasang daripada peta konfigurasi, tetapi mengenainya kemudian.

Jadi sekarang kita perlu membina imej asli dan menolaknya ke repositori jauh.

Petua nombor 1

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

Mengkonfigurasi GraalVM seperti itu supaya anda akan mempunyai binaan terpantas dengan penggunaan memori paling rendah (sekitar 2GB). Bagi saya ia adalah satu kemestian kerana saya hanya mempunyai 16GB memori dan banyak barangan yang dipasang.

Petua nombor 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>
Salin selepas log masuk
  • 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>
Salin selepas log masuk

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 {
}
Salin selepas log masuk
public record WebServingSpec(String page1, String page2) {
}
Salin selepas log masuk
public record WebServingStatus (String status) {
}
Salin selepas log masuk

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();
Salin selepas log masuk

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);
    }
}
Salin selepas log masuk

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;
    }
Salin selepas log masuk

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;
}
Salin selepas log masuk

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));
}
Salin selepas log masuk

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

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

Salin selepas log masuk

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();
}
Salin selepas log masuk

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

Salin selepas log masuk

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
Salin selepas log masuk

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>
Salin selepas log masuk

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

Atas ialah kandungan terperinci Menulis koperator di Jawa. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!