汗をかくことなくJavaでKubernetesオペレーターを開発

JavaでKubernetesオペレーターを開発することは、まだ一般的ではありません。 これまでのところ、Goは、特に対応するテストを書くための優れたサポートのために、ここで選択されている言語です。 

Javaベースのプロジェクト開発における課題の1つは、Kubernetes APIサーバーと対話する簡単な自動統合テストがないことでした。 しかし、広く使用されている Testcontainers 統合テストライブラリをベースにしたオープンソースライブラリ Kindcontainer のおかげで、このギャップを埋めることができ、JavaベースのKubernetesプロジェクトの開発を容易にすることができます。 

この記事では、Testcontainers を使用して、Java で実装されたカスタム Kubernetes コントローラーとオペレーターをテストする方法を紹介します。

2400X1260 汗をかくことなく Java で Kubernetes オペレーターを開発

DockerでのKubernetes

Testcontainersを使用すると、Java仮想マシン(JVM)内で実行されているテストから、Dockerコンテナで実行されている任意のインフラストラクチャコンポーネントとプロセスを起動できます。 フレームワークは、Dockerコンテナのライフサイクルとクリーンアップをテスト実行にバインドします。 たとえば、デバッグ中にJVMが突然終了した場合でも、起動したDockerコンテナも停止および削除されます。 Testcontainersは、Dockerイメージの汎用クラスに加えて、高度な構成オプションを持つコンポーネントなど、サブクラスの形式で特殊な実装を提供します。 

これらの特殊な実装は、サードパーティのライブラリによって提供することもできます。 オープンソースプロジェクトのKindcontainerは、Testcontainersをベースにした様々なKubernetesコンテナに特化した実装を提供するサードパーティライブラリの1つです。

  • ApiServerコンテナ
  • K3sContainer
  • 種類コンテナ

ただし ApiServerContainer 、Kubernetesコントロールプレーンのごく一部、つまりKubernetes APIサーバーのみを提供し、 K3sContainer KindContainer Dockerコンテナで完全な単一ノードKubernetesクラスターを起動することに重点を置いています。 

これにより、それぞれのテストの要件に応じてトレードオフが可能になります:テストにAPIサーバーとの対話のみが必要な場合は、通常、大幅に高速な起動 ApiServerContainer で十分です。 ただし、Kubernetesコントロールプレーンの他のコンポーネントや他のオペレーターとの複雑な相互作用をテストすることが範囲内にある場合は、起動時間を犠牲にしてでも、2つの「より大きな」実装がそれに必要なツールを提供します。 ちなみに、ハードウェア構成によっては、起動時間が1分以上に達することがあります。

最初の例

Kubernetesコンテナに対するテストがいかに簡単かを説明するために、JUnit 5を使用した例を見てみましょう。

@Testcontainers
public class SomeApiServerTest {
  @Container
  public ApiServerContainer<?> K8S = new ApiServerContainer<>();

  @Test
  public void verify_no_node_is_present() {
    Config kubeconfig = Config.fromKubeconfig(K8S.getKubeconfig());
    try (KubernetesClient client = new KubernetesClientBuilder()
           .withConfig(kubeconfig).build()) {
      // Verify that ApiServerContainer has no nodes
      assertTrue(client.nodes().list().getItems().isEmpty());
    }
  }
}

JUnit 5拡張機能のおかげで@Testcontainers、管理する必要があるコンテナをアノテーションで@Containerマークすることで、のApiServerContainerライフサイクル管理を簡単に処理できます。コンテナが起動すると、APIサーバーとの接続を確立するために必要な詳細を含むYAMLドキュメントをメソッドを介して getKubeconfig() 取得できます。 

このYAMLドキュメントは、Kubernetesの世界で接続情報を提示する標準的な方法を表しています。 この例で使用されているファブリック8 Kubernetes クライアントは、 を使用して Config.fromKubeconfig()構成できます。 他のKubernetesクライアントライブラリは、同様のインターフェイスを提供します。 Kindcontainerは、この点に関して特定の要件を課すものではありません。

これら 3 つのコンテナー実装はすべて、共通の API に依存しています。 したがって、開発の後の段階で、より重い実装の 1 つがテストに必要であることが明らかになった場合は、それ以上コードを変更することなく、その実装に切り替えるだけで済みます。

テストコンテナのカスタマイズ

多くの場合、Kubernetesコンテナが起動した後、実際のテストケースを開始する前に、多くの準備作業を行う必要があります。 たとえば、オペレーターの場合、API サーバーは最初にカスタム リソース定義 (CRD) を認識するか、Helm チャートを使用して別のコントローラーをインストールする必要があります。 最初は複雑に聞こえるかもしれませんが、Kindcontainer と、コマンドライン ツールkubectlhelmと .

次のリストは、テストのクラスパス kubectlから を使用して最初に CRD を適用し、その後に Helm チャートをインストールする方法を示しています。

@Testcontainers
public class FluentApiTest {
  @Container
  public static final K3sContainer<?> K3S = new K3sContainer<>()
    .withKubectl(kubectl -> {
      kubectl.apply.fileFromClasspath(“manifests/mycrd.yaml”).run();
    })
    .withHelm3(helm -> {
      helm.repo.add.run(“repo”, “https://repo.example.com”);
      helm.repo.update.run();
      helm.install.run(“release”, “repo/chart”);
    );
  // Tests go here
}

Kindcontainer は、最初のテストが始まる前にすべてのコマンドが実行されるようにします。 コマンド間に依存関係がある場合は、簡単に解決できます。Kindcontainer は、指定された順序で実行されることを保証します。

Fluent API は、それぞれのコマンドライン ツールの呼び出しに変換されます。 これらは個別のコンテナで実行され、必要な接続の詳細で自動的に開始され、Docker内部ネットワークを介してKubernetesコンテナに接続されます。 このアプローチにより、Kubernetes イメージへの依存関係と、その中で使用可能なツールに関するバージョンの競合が回避されます。

Kubernetes のバージョンの選択

開発者が他に何も指定しない場合、Kindcontainer はデフォルトでサポートされている最新の Kubernetes バージョンを起動します。 ただし、このアプローチは一般的に推奨されないため、ベスト プラクティスでは、次に示すように、コンテナーを作成するときに、サポートされているバージョンの 1 つを明示的に指定する必要があります。

@Testcontainers
public class SpecificVersionTest {
  @Container
  KindContainer<?> container = new KindContainer<>(KindContainerVersion.VERSION_1_24_1);
  // Tests go here
}

3 つのコンテナー実装にはそれぞれ独自の Enum、サポートされている Kubernetes バージョンの 1 つを選択できます。 Kindcontainerプロジェクト自体のテストスイートは、精巧なマトリックスベースの統合テストセットアップの助けを借りて、これらの各バージョンで完全な機能セットを簡単に利用できることを保証します。 Kubernetesエコシステムは急速に進化し、Kubernetesのバージョンに応じて異なる初期化手順を実行する必要があるため、この精巧なテストプロセスが必要です。

一般的に、このプロジェクトは、 4 か月ごとにリリースされる、現在保守されているすべてのKubernetesメジャーバージョンのサポートに重点を置いています。 古いKubernetesバージョンは、 @Deprecated Kindcontainerでのサポートが負担になりすぎると、最終的に削除されます。 ただし、これは、それぞれのKubernetesバージョンの使用が推奨されなくなった場合にのみ発生します。

独自のDockerレジストリを持ち込む

パブリックソースからのDockerイメージへのアクセスは、特に手動または自動の監査を備えた内部Dockerレジストリに依存している企業環境では、多くの場合、簡単ではありません。 Kindcontainerを使用すると、開発者はこの目的で使用されるDockerイメージに独自の座標を指定できます。 ただし、Kindcontainer は、初期化手順が異なる可能性があるため、使用されている Kubernetes のバージョンを認識する必要があるため、これらのカスタム座標がそれぞれの Enum 値に追加されます。

@Testcontainers
public class CustomKubernetesImageTest {
  @Container
  KindContainer<?> container = new KindContainer<>(KindContainerVersion.VERSION_1_24_1
    .withImage(“my-registry/kind:1.24.1”));
  // Tests go here
}

Kubernetesイメージ自体に加えて、Kindcontainerは他のいくつかのDockerイメージも使用します。 すでに説明したように、 や helm などのkubectlコマンドラインツールは、独自のコンテナで実行されます。適切に、これらのツールに必要なDockerイメージも構成できます。 幸いなことに、バージョンに依存するコード パスは実行に必要ありません。 

したがって、以下に示す構成は、Kubernetes イメージの場合よりも単純です。

@Testcontainers
public class CustomFluentApiImageTest {
  @Container
  KindContainer<?> container = new KindContainer<>()
    .withKubectlImage(
      DockerImageName
        .parse(“my-registry/kubectl:1.21.9-debian-10-r10”))
    .withHelm3Image(DockerImageName.parse(“my-registry/helm:3.7.2”));
  // Tests go here
}

開始された他のすべてのコンテナーのイメージの座標も、手動で簡単に選択できます。 ただし、同じ画像または少なくとも互換性のある画像の使用を保証することは、常に開発者の責任です。 この目的のために、使用されるDockerイメージとそのバージョンの完全なリストは、GitHubのKindcontainerのドキュメントにあります。

アドミッション コントローラー Webhook

ここまで示したテストシナリオでは、JVMで実行されているKubernetesクライアントが、ローカルまたはリモートで実行されているKubernetesコンテナにネットワーク経由でアクセスし、その内部で実行されているAPIサーバーと通信するという通信方向が明確です。 Dockerは、APIサーバー用のDockerコンテナにポートが開かれ、アクセス可能になるという、この標準的なケースを非常に簡単にします。 

Kindcontainer は、このプロセスに必要な構成手順を自動的に実行し、それぞれのネットワーク構成に適した接続情報を Kubeconfig として提供します。

ただし、アドミッション コントローラー Webhook は、技術的により困難なテスト シナリオを提示します。 これらの場合、API サーバーは、マニフェストを処理するときに HTTPS 経由で外部 Webhook と通信できる必要があります。 私たちの場合、これらのWebhookは通常、テストロジックが実行されるJVMで実行されます。 ただし、Dockerコンテナから簡単にアクセスできない場合があります。

これらの Webhook のテストをネットワーク設定とは無関係に容易にし、かつシンプルにするために、Kindcontainer はトリックを採用しています。 Kubernetes コンテナー自体に加えて、さらに 2 つのコンテナーが開始されます。 SSHサーバーは、テストJVMからKubernetesコンテナへのトンネルを確立し、リバースポート転送を設定する機能を提供し、APIサーバーがJVMと通信できるようにします。 

Kubernetes では Webhook との TLS で保護された通信が必要なため、Webhook の TLS ターミネーションを処理するために Nginx コンテナーも開始されます。 Kindcontainer は、これに必要な証明書資料の管理を管理します。 

プロセス、コンテナ、およびそれらのネットワーク通信のセットアップ全体を図 1に示します。

Webhookをテストするためのネットワーク設定の図で、左側にWebhookサーバー、sshクライアント、junit test、右側にdockerネットワークとsshサーバー、nginxコンテナ、kubernetesコンテナが表示されています。
図 1: Webhook をテストするためのネットワーク設定。

幸いなことに、Kindcontainer はこの複雑さを使いやすい API の背後に隠しています。

@Testcontainers
public class WebhookTest {
    @Container
    ApiServerContainer<?> container = new ApiServerContainer<>()
.withAdmissionController(admission -> {
        admission.mutating()
                .withNewWebhook("mutating.example.com")
                .atPort(webhookPort) // Local port of webhook
                .withNewRule()
                .withApiGroups("")
                .withApiVersions("v1")
                .withOperations("CREATE", "UPDATE")
                .withResources("configmaps")
                .withScope("Namespaced")
                .endRule()
                .endWebhook()
                .build();
    });

    // Tests go here
}

開発者は、ローカルで実行されているWebhookのポートと、Kubernetesで設定するために必要な情報を提供するだけで済みます。 その後、Kindcontainerは、SSHトンネリング、TLSターミネーション、およびKubernetesの構成を自動的に処理します。

Javaについて考えてみましょう

最小限のJUnitテストの簡単な例から始めて、Javaで実装されたカスタムKubernetesコントローラーとオペレーターをテストする方法を示しました。 Fluent APIの助けを借りてエコシステムの使い慣れたコマンドラインツールを使用する方法と、制限されたネットワーク環境でも統合テストを簡単に実行する方法について説明しました。 最後に、アドミッションコントローラーのWebhookをテストするという技術的に困難なユースケースでさえ、Kindcontainerを使用して簡単かつ便利に実装できることを示しました。 

これらの新しいテストの可能性のおかげで、将来的には、より多くの開発者がKubernetes関連プロジェクトの言語としてJavaを検討するようになることを願っています。

さらに詳しく