Skip to content

Java Operator SDKプロジェクトセットアップ

Java Operator SDKで実装するカスタムコントローラーのプロジェクトのセットアップ方法について記載します。

クラスタ作成

  • kind/cluster/josdk-cluster.yamlを下記内容で作成します。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
  • KindでKubernetesクラスタを構築します。
kind create cluster --name josdk-cluster --config kind/cluster/josdk-cluster.yaml

プロジェクト作成

  • プロジェクトを初期化します。
mvn archetype:generate \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DjavaCompilerVersion=21 \
    -DarchetypeVersion=1.5 \
    -DinteractiveMode=false \
    -DgroupId=nob.example \
    -DartifactId=nob-controller
  • pom.xmlを下記で作成します。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>nob.example</groupId>
    <artifactId>nob-controller</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>nob-controller</name>
    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>${maven.compiler.release}</maven.compiler.source>
        <maven.compiler.target>${maven.compiler.release}</maven.compiler.target>
        <josdk.version>5.2.2</josdk.version>
        <slf4j.version>2.0.17</slf4j.version>
        <junit.version>5.9.2</junit.version>
        <log4j.version>2.25.3</log4j.version>
        <lombok.version>1.18.40</lombok.version>
        <fabric8-client.version>7.4.0</fabric8-client.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.javaoperatorsdk</groupId>
                <artifactId>operator-framework-bom</artifactId>
                <version>${josdk.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.javaoperatorsdk</groupId>
            <artifactId>operator-framework</artifactId>
            <version>${josdk.version}</version>
        </dependency>
        <dependency>
            <groupId>io.javaoperatorsdk</groupId>
            <artifactId>operator-framework-junit-5</artifactId>
            <version>${josdk.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
            </plugin>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>crd-generator-maven-plugin</artifactId>
                <version>${fabric8-client.version}</version>
                <executions>
                    <execution>
                        <?m2e execute onConfiguration?>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

実装

NobSpec.java

カスタムリソースのSpecを宣言します。

package nob.example;

import lombok.Data;

@Data
public class NobSpec {

    private String deploymentName;

    private Integer replicas;
}

NobStatus.java

カスタムリソースのStatusを宣言します。

package nob.example;

import lombok.Data;

@Data
public class NobStatus {

    private Integer availableReplicas;
}

Nob.java

カスタムリソースを宣言します。

package nob.example;

import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("nob.example")
@Version("v1")
public class Nob extends CustomResource<NobSpec, NobStatus> implements Namespaced {
}

DeploymentDependentResource.java

カスタムリソースが管理するDeploymentの定義を宣言します。

package nob.example;

import java.util.Map;

import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class DeploymentDependentResource extends CRUDKubernetesDependentResource<Deployment, Nob> {

    @Override
    protected Deployment desired(Nob nob, Context<Nob> context) {
        return new DeploymentBuilder()
                .withMetadata(new ObjectMetaBuilder()
                        .withName(nob.getSpec().getDeploymentName())
                        .withNamespace(nob.getMetadata().getNamespace())
                        .withLabels(Map.of("app", "nob-nginx", "controller", nob.getMetadata().getName()))
                        .withOwnerReferences(new OwnerReferenceBuilder()
                                .withApiVersion(nob.getApiVersion())
                                .withKind(nob.getKind())
                                .withName(nob.getMetadata().getName())
                                .withUid(nob.getMetadata().getUid())
                                .withController()
                                .withBlockOwnerDeletion()
                                .build())
                        .build())
                .withSpec(new DeploymentSpecBuilder()
                        .withSelector(new LabelSelectorBuilder()
                                .withMatchLabels(
                                        Map.of("app", "nob-nginx", "controller", nob.getMetadata().getName()))
                                .build())
                        .withReplicas(nob.getSpec().getReplicas())
                        .withTemplate(new PodTemplateSpecBuilder()
                                .withMetadata(new ObjectMetaBuilder()
                                        .withLabels(Map.of("app", "nob-nginx", "controller",
                                                nob.getMetadata().getName()))
                                        .build())
                                .withSpec(new PodSpecBuilder()
                                        .withContainers(new ContainerBuilder()
                                                .withName("nob-nginx")
                                                .withImage("nginx:1.18")
                                                .build())
                                        .build())
                                .build())
                        .build())
                .build();
    }
}

NobReconciler.java

カスタムコントローラーのビジネスロジック本体を実装します。

package nob.example;

import java.util.Map;

import io.fabric8.kubernetes.api.model.Event;
import io.fabric8.kubernetes.api.model.EventBuilder;
import io.fabric8.kubernetes.api.model.EventSourceBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentList;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;

@Workflow(dependents = { @Dependent(type = DeploymentDependentResource.class) })
public class NobReconciler implements Reconciler<Nob> {

    @Override
    public UpdateControl<Nob> reconcile(Nob nob, Context<Nob> context) throws Exception {

        // 古いdeploymentを削除
        DeploymentList deploymentList = context.getClient().apps().deployments()
                .inNamespace(nob.getMetadata().getNamespace())
                .withLabels(Map.of("app", "nob-nginx", "controller", nob.getMetadata().getName())).list();
        for (Deployment d : deploymentList.getItems()) {
            if (!d.getMetadata().getName().equals(nob.getSpec().getDeploymentName())) {
                context.getClient().apps().deployments().inNamespace(d.getMetadata().getNamespace())
                        .withName(d.getMetadata().getName()).delete();
            }
        }

        // Status更新向けに作成したDeploymentを取得
        Deployment deployment = context.getClient().apps().deployments().inNamespace(nob.getMetadata().getNamespace())
                .withName(nob.getSpec().getDeploymentName()).get();
        if (deployment == null) {
            return UpdateControl.noUpdate();
        }

        // Statusがセットされていなければ作成
        if (nob.getStatus() == null) {
            NobStatus n = new NobStatus();
            n.setAvailableReplicas(0);
            nob.setStatus(n);
        }

        // ステータスが変わらなければ何もせず終了
        if (deployment.getStatus().getAvailableReplicas() == null) {
            return UpdateControl.noUpdate();
        }
        if (deployment.getStatus().getAvailableReplicas().equals(nob.getStatus().getAvailableReplicas())) {
            return UpdateControl.noUpdate();
        }

        // Status更新
        nob.getStatus().setAvailableReplicas(deployment.getStatus().getAvailableReplicas());

        ObjectReference involvedObject = new ObjectReference();
        involvedObject.setApiVersion(nob.getApiVersion());
        involvedObject.setKind(nob.getKind());
        involvedObject.setName(nob.getMetadata().getName());
        involvedObject.setNamespace(nob.getMetadata().getNamespace());
        involvedObject.setUid(nob.getMetadata().getUid());
        Event event = new EventBuilder()
                .withMetadata(new ObjectMetaBuilder()
                        .withGenerateName("nobcustomresource-")
                        .withNamespace(nob.getMetadata().getNamespace())
                        .build())
                .withType("Normal")
                .withReason("Updated")
                .withMessage("Update status.availableReplicas: " + nob.getStatus().getAvailableReplicas())
                .withInvolvedObject(involvedObject)
                .withCount(1)
                .withSource(new EventSourceBuilder()
                        .withComponent(nob.getMetadata().getName())
                        .build())
                .build();
        context.getClient().v1().events().inNamespace(nob.getMetadata().getNamespace()).resource(event).create();

        return UpdateControl.patchStatus(nob);
    }
}

App.java

コントローラーのエントリポイントです。

package nob.example;

import io.javaoperatorsdk.operator.Operator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {

    private static final Logger log = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        Operator operator = new Operator();
        operator.register(new NobReconciler());
        operator.start();
        log.info("Operator started.");
    }
}

log4j2.xml

ログ出力に関する設定main/resources/log4j2.xmlを下記内容で作成します。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="Config" status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %threadId %-30c{1.} [%-5level] %msg%n%throwable"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger level="debug" name="nob.example" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

単体テスト

NobReconcilerTest.java

コントローラーのテストを記載します。mvn clean testでCRDのマニフェストを自動生成しつつテストを実行します。

package nob.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

public class NobReconcilerTest {

    @RegisterExtension
    LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder()
            .withReconciler(NobReconciler.class)
            .build();

    private static final String DEPLOYMENT_NAME = "test-nginx";
    private static final String UPDATED_DEPLOYMENT_NAME = "updated-test-nginx";

    @Test
    void test() {

        Nob cr = extension.create(testResource());

        await().untilAsserted(() -> {
            Deployment d = extension.get(Deployment.class, DEPLOYMENT_NAME);
            assertThat(d).isNotNull();
            assertThat(d.getSpec().getReplicas()).isEqualTo(cr.getSpec().getReplicas());
        });

        cr.getSpec().setDeploymentName(UPDATED_DEPLOYMENT_NAME);
        cr.getSpec().setReplicas(5);
        extension.replace(cr);

        await().untilAsserted(() -> {
            Deployment d = extension.get(Deployment.class, UPDATED_DEPLOYMENT_NAME);
            assertThat(d).isNotNull();
            assertThat(d.getMetadata().getName()).isEqualTo(cr.getSpec().getDeploymentName());
            assertThat(d.getSpec().getReplicas()).isEqualTo(cr.getSpec().getReplicas());
        });

        extension.delete(cr);

        await().untilAsserted(() -> {
            Deployment d = extension.get(Deployment.class, UPDATED_DEPLOYMENT_NAME);
            assertThat(d).isNull();
        });
    }

    Nob testResource() {
        Nob nob = new Nob();
        nob.setMetadata(new ObjectMetaBuilder()
                .withName("test-resource")
                .build());
        nob.setSpec(new NobSpec());
        nob.getSpec().setDeploymentName(DEPLOYMENT_NAME);
        nob.getSpec().setReplicas(3);
        return nob;
    }
}

ローカルアプリケーション起動

  • アプリのビルドおよびCRDマニフェストの作成を行います。
mvn clean install
  • CRDのマニフェストをapplyします。
kubectl apply -f target/classes/META-INF/fabric8/nobs.nob.example-v1.yml
  • カスタムリソースのマニフェストsrc/k8s/custom-resource.ymlを下記で作成してapplyします。
apiVersion: nob.example/v1
kind: Nob
metadata:
  name: nob-sample
spec:
  deploymentName: nob-nginx
  replicas: 3
kubectl apply -f src/k8s/custom-resource.yml
  • 下記コマンドでカスタムリソースが動いていることを確認できます。
$ kubectl get Nob
NAME         AGE
nob-sample   17s
  • Javaプロセスとしてコントローラーを起動します。
mvn exec:java -Dexec.mainClass="nob.example.App"
  • コントローラーが正常に動作していれば、nginxのPodが作成されます。
$ kubectl get pod
NAME                         READY   STATUS    RESTARTS   AGE
nob-nginx-57466cd967-2j56f   1/1     Running   0          6s
nob-nginx-57466cd967-rz2rv   1/1     Running   0          6s
nob-nginx-57466cd967-vhssd   1/1     Running   0          6s