Rumah >Java >javaTutorial >Menulis koperator di Jawa
Tutorial ini khusus untuk pembangun dengan latar belakang Java yang ingin belajar cara menulis operator kubernetes pertama dengan pantas. Mengapa pengendali? Terdapat beberapa kelebihan:
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.
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.
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.
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:
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); } }
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>
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>
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.
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!