Kubernetes で Spring Boot 実行

構築するもの

Spring 環境の Kubernetes は成熟しつつあります。2024 Spring 調査状況 (英語) によると、回答者の 65% が Spring 環境で Kubernetes を使用しています。

Kubernetes 上で Spring Boot アプリケーションを実行する前に、まずコンテナーイメージを生成する必要があります。Spring Boot は、Cloud Native Buildpacks を使用して、Maven または Gradle プラグインから Docker イメージを簡単に生成することをサポートしています。

このガイドの目的は、Kubernetes で Spring Boot アプリケーションを実行し、クラウドネイティブアプリケーションを構築できるいくつかのプラットフォーム機能を利用する方法を示すことです。

このガイドでは、2 つの Spring Boot Web アプリケーションを構築します。Cloud Native Buildpacks を使用して各 Web アプリケーションを Docker イメージにパッケージ化し、そのイメージに基づいて Kubernetes デプロイを作成し、デプロイにアクセスするためのサービスを作成します。

必要なもの

  • Eclipse STSIntelliJ IDEA のような任意の IDE または VSCode のようなテキストエディター

  • Java 17 以降

  • Docker 環境

  • Kubernetes 環境

Docker デスクトップ (英語) は、このガイドに従うために必要な Docker 環境と Kubernetes 環境の両方を提供します。

このガイドを完了する方法

このガイドでは、Kubernetes 上で Spring Boot アプリを実行するために必要な成果物の作成に重点を置いています。そのため、このリポジトリ [GitHub] (英語) で提供されているコードを使用するのが最善の方法です。

このリポジトリは、使用する 2 つのサービスを提供します。

  • hello-spring-k8s は、Hello World メッセージをエコーする基本的な Spring Boot REST アプリケーションです。

  • hello-caller は、Spring Boot REST アプリケーション hello-spring-k8s を呼び出します。hello-caller サービスは、Kubernetes 環境でサービス検出がどのように機能するかを示すためのものです。

これら 2 つのアプリケーションはどちらも Spring Boot REST アプリケーションであり、このガイドを使用して最初から作成できます。このガイドに固有のコードは、レッスンの進行に合わせて以下で紹介されます。

このガイドは個別のセクションに分かれています。

ソリューションリポジトリには、Kubernetes アーティファクトがすでに作成されています。このガイドでは、これらのオブジェクトの作成方法を段階的に説明しますが、実際の例についてはいつでもソリューションを参照できます。

Docker イメージを生成する

まず、Cloud Native Buildpacks を使用して hello-spring-k8s プロジェクトの Docker イメージを生成します。hello-spring-k8s ディレクトリで、次のコマンドを実行します。

$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-spring-k8s

これにより、spring-k8s/hello-spring-k8s という名前の Docker イメージが生成されます。ビルドが完了すると、アプリケーション用の Docker イメージが作成され、次のコマンドで確認できます。

$ docker images spring-k8s/hello-spring-k8s

REPOSITORY                    TAG       IMAGE ID       CREATED        SIZE
spring-k8s/hello-spring-k8s   latest    <ID>        44 years ago   325MB

これで、コンテナーイメージを開始し、それが機能することを確認できます。

$ docker run -p 8080:8080 --name hello-spring-k8s -t spring-k8s/hello-spring-k8s

アクチュエーター / ヘルスエンドポイントに HTTP リクエストを送信することで、すべてが機能していることをテストできます。

$ curl http://localhost:8080/actuator/health

{"status":"UP"}

先に進む前に、実行中のコンテナーを必ず停止してください。

$ docker stop hello-spring-k8s

Kubernetes の要件

アプリケーションのコンテナーイメージ(start.spring.io にアクセスするだけです! )を使用して、アプリケーションを Kubernetes で実行する準備が整いました。これを行うには、2 つのことが必要です。

  1. Kubernetes CLI (kubectl)

  2. アプリケーションをデプロイする Kubernetes クラスター

次の手順 (英語) に従って、KubernetesCLI をインストールします。

どの Kubernetes クラスターでも動作しますが、この記事では、できるだけ簡単にするために、ローカルでクラスターを起動します。Kubernetes クラスターをローカルで実行する最も簡単な方法は、Docker デスクトップ (英語) を使用することです。

チュートリアル全体で使用される一般的な Kubernetes フラグには、注目すべきものがあります。--dry-run=client フラグは、送信するオブジェクトを送信せずに出力のみするように Kubernetes に指示します。-o yaml フラグは、コマンドの出力が yaml であることを指定します。これら 2 つのフラグは、出力リダイレクト > と組み合わせて使用され、Kubernetes コマンドをファイルにキャプチャーできます。これは、オブジェクトを作成する前に編集したり、繰り返し可能なプロセスを作成したりする場合に役立ちます。

Kubernetes へのデプロイ

このセクションのソリューションは k8s-artifacts/basic/* で定義されています。

hello-spring-k8s アプリケーションを Kubernetes にデプロイするには、Kubernetes がアプリケーションのデプロイ、実行、管理に使用できる YAML を生成するとともに、そのアプリケーションをクラスターの残りの部分に公開する必要があります。

提供されたソリューションを実行する代わりに、自分で yaml をビルドすることを選択した場合は、まず YAML 用のディレクトリを作成します。生成される yaml ファイルはパスに依存しないため、このフォルダーがどこにあるかは問題ではありません。

$ mkdir k8s
$ cd k8s

これで、kubectl を使用して、必要な基本的な YAML を生成できます。

$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/spring-k8s/hello-spring-k8s:latest -o yaml --dry-run=client > deployment.yaml

使用しているイメージはローカルなので、デプロイ内のコンテナーの imagePullPolicy (英語) を変更する必要があります。yaml の containers: 仕様は次のようになります。

    spec:
      containers:
      - image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}

imagePullPolicy を変更せずに デプロイを実行しようとすると、pod のステータスは ErrImagePull になります。

deployment.yaml ファイルは Kubernetes にアプリケーションのデプロイと管理の方法を指示しますが、アプリケーションを他のアプリケーションへのネットワークサービスにすることはできません。そのためには、サービスリソースが必要です。Kubectl は、サービスリソースの YAML を生成できます。

$ kubectl create service clusterip gs-spring-boot-k8s --tcp 80:8080 -o yaml --dry-run=client > service.yaml

これで、YAML ファイルを Kubernetes に適用する準備が整いました。

$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

次に、実行できます。

$ kubectl get all

新しく作成されたデプロイ、サービス、pod が実行されていることがわかります。

NAME                                      READY   STATUS    RESTARTS   AGE
pod/gs-spring-boot-k8s-779d4fcb4d-xlt9g   1/1     Running   0          3m40s

NAME                         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/gs-spring-boot-k8s   ClusterIP   10.96.142.74   <none>        80/TCP    3m40s
service/kubernetes           ClusterIP   10.96.0.1      <none>        443/TCP   4h55m

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/gs-spring-boot-k8s   1/1     1            1           3m40s

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/gs-spring-boot-k8s-779d4fcb4d   1         1         1       3m40s

残念ながら、Kubernetes のサービスはクラスターネットワークの外部に公開されていないため、HTTP リクエストを直接行うことはできません。kubectl の助けを借りて、ローカルマシンからクラスターで実行されているサービスに HTTP トラフィックを転送できます。

$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80

port-forward コマンドを実行すると、localhost:9090 に HTTP リクエストを送信できるようになり、Kubernetes で実行されているサービスに転送されます。

$ curl http://localhost:9090/helloWorld
Hello World!!

先に進む前に、必ず上記の port-forward コマンドを停止してください。

ベストプラクティス

このセクションのソリューションは k8s-artifacts/best_practice/* で定義されています。

アプリケーションは Kubernetes 上で実行されますが、アプリケーションを最適に実行するには、ベストプラクティスを実装することをお勧めします。

テキストエディターで deployment.yaml を開き、準備プロパティと有効プロパティをファイルに追加します。

k8s/ デプロイ .yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gs-spring-boot-k8s
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: gs-spring-boot-k8s
    spec:
      containers:
      - image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
status: {}

これは最初のベストプラクティスに対応します。さらに、アプリケーション設定にプロパティを追加する必要があります。アプリケーションは Kubernetes 上で実行されるため、優れたクラウド開発者が行うように、Kubernetes ConfigMaps (英語) を利用してこのプロパティを外部化できます。次に、その方法を見ていきます。

ConfigMaps を使用した設定の外部化

このセクションのソリューションは k8s-artifacts/config_map/* で定義されています。

Spring Boot アプリケーションで正常なシャットダウンを有効にするには、application.properties に server.shutdown=graceful を設定します。この行をコードに直接追加するのではなく、ConfigMap (英語) を使用します。アクチュエーターエンドポイントを使用して、アプリケーションが ConfigMap のプロパティファイルを PropertySources のリストに追加していることを確認できます。

グレースフルシャットダウンを可能にし、すべてのアクチュエーターエンドポイントを公開するプロパティファイルを作成できます。アプリケーションがプロパティファイルを ConfigMap から PropertySources のリストに追加していることを確認する方法として、アクチュエーターエンドポイントを使用できます。

yaml ファイルを保存する場所に、application.properties という新しいファイルを作成します。そのファイルに次のプロパティを追加します。

application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*

または、次のコマンドを実行して、コマンドラインから 1 つの簡単な手順でこれを実行できます。

$ cat <<EOF >./application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
EOF

プロパティファイルを作成したら、kubectl を使用して ConfigMap を作成できます (英語)

$ kubectl create configmap gs-spring-boot-k8s --from-file=./application.properties

ConfigMap を作成すると、次のようになります。

$ kubectl get configmap gs-spring-boot-k8s -o yaml
apiVersion: v1
data:
  application.properties: |
    server.shutdown=graceful
    management.endpoints.web.exposure.include=*
kind: ConfigMap
metadata:
  creationTimestamp: "2020-09-10T21:09:34Z"
  name: gs-spring-boot-k8s
  namespace: default
  resourceVersion: "178779"
  selfLink: /api/v1/namespaces/default/configmaps/gs-spring-boot-k8s
  uid: 9be36768-5fbd-460d-93d3-4ad8bc6d4dd9

これを行うには、デプロイ YAML を変更して、最初にボリュームを作成してから、そのボリュームをコンテナーにマウントする必要があります。

k8s/ デプロイ .yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gs-spring-boot-k8s
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: gs-spring-boot-k8s
    spec:
      containers:
      - image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
        volumeMounts:
          - name: config-volume
            mountPath: /workspace/config
      volumes:
        - name: config-volume
          configMap:
            name: gs-spring-boot-k8s
status: {}

すべてのベストプラクティスを実装したら、新しいデプロイを Kubernetes に適用できます。これにより、別の Pod がデプロイされ、古い Pod が停止します(新しい Pod が正常に起動する限り)。

$ kubectl apply -f deployment.yaml

活性プローブと準備プローブが正しく構成されている場合、Pod は正常に起動し、準備完了状態に移行します。Pod が準備完了状態に到達しない場合は、戻って準備プローブの構成を確認してください。Pod が準備完了状態に達しても、Kubernetes が常に Pod を再起動する場合は、活性プローブが正しく構成されていません。pod が起動して起動したままの場合、すべてが正常に機能しています。

/actuator/env エンドポイントを押すことにより、ConfigMap ボリュームがマウントされていること、およびアプリケーションがプロパティファイルを使用していることを確認できます。

$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80

http://localhost:9090/actuator/env にアクセスすると、マウントされたボリュームから提供されたプロパティソースが表示されます。

curl http://localhost:9090/actuator/env | jq
{
   "name":"applicationConfig: [file:./config/application.properties]",
   "properties":{
      "server.shutdown":{
         "value":"graceful",
         "origin":"URL [file:./config/application.properties]:1:17"
      },
      "management.endpoints.web.exposure.include":{
         "value":"*",
         "origin":"URL [file:./config/application.properties]:2:43"
      }
   }
}

続行する前に、必ず port-forward コマンドを停止してください。

サービス検出と負荷分散

ガイドのこのパートでは、hello-caller アプリケーションを追加します。このセクションのソリューションは、k8s-artifacts/service_discovery/* で定義されています。

負荷分散を実証するために、まず既存の hello-spring-k8s サービスを 3 つのレプリカに拡張してみましょう。これは、デプロイに replicas 構成を追加することで実行できます。

...
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 3
  selector:
...

次のコマンドを実行して デプロイを更新します。

kubectl apply -f deployment.yaml

これで、3 つの pods が動作しているのがわかるはずです。

$ kubectl get pod --selector=app=gs-spring-boot-k8s

NAME                                  READY   STATUS    RESTARTS   AGE
gs-spring-boot-k8s-76477c6c99-2psl4   1/1     Running   0          15m
gs-spring-boot-k8s-76477c6c99-ss6jt   1/1     Running   0          3m28s
gs-spring-boot-k8s-76477c6c99-wjbhr   1/1     Running   0          3m28s

このセクションでは 2 番目のサービスを実行する必要があるため、hello-caller に注目しましょう。このアプリケーションには、hello-spring-k8s を呼び出す単一のエンドポイントがあります。URL は kubernetes のサービス名と同じであることに注意してください。

	@GetMapping
	public Mono<String> index() {
		return webClient.get().uri("http://gs-spring-boot-k8s/name")
				.retrieve()
				.toEntity(String.class)
				.map(entity -> {
					String host = entity.getHeaders().get("k8s-host").get(0);
					return "Hello " + entity.getBody() + " from " + host;
				});

	}

Kubernetes は DNS エントリを設定して、hello-spring-k8s のサービス ID を使用して、pods の IP アドレスを知らなくてもサービスに HTTP リクエストを送信できるようにします。Kubernetes サービスは、これらのリクエストをすべての pods 間で負荷分散します。

ここで、hello-caller アプリケーションを Docker イメージとしてパッケージ化し、Kubernetes リソースとして実行する必要があります。Docker イメージを生成するには、もう一度 Cloud Native Buildpacks を使用します。hello-caller フォルダーで、次のコマンドを実行します。

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-caller

Docker イメージが作成されると、すでに見たものと同様の新しい デプロイを作成できます。完成した構成は caller_deployment.yaml ファイルに用意されています。このファイルを実行します。

kubectl apply -f caller_deployment.yaml

次のコマンドでアプリが実行されていることを確認できます。

$ kubectl get pod --selector=app=gs-hello-caller

NAME                               READY   STATUS    RESTARTS   AGE
gs-hello-caller-774469758b-qdtsx   1/1     Running   0          2m34s

提供されているファイル caller_service.yaml で定義されているサービスも作成する必要があります。このファイルは次のコマンドで実行できます。

kubectl apply -f caller_service.yaml

これで、2 つの デプロイと 2 つのサービスが実行されたため、アプリケーションをテストする準備が整いました。

$ kubectl port-forward svc/gs-hello-caller 9090:80

$ curl http://localhost:9090 -i; echo

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT

Hello Paul from gs-spring-boot-k8s-76477c6c99-5xdq8

複数のリクエストを行うと、異なる名前が返されます。リクエストには pod の名前もリストされます。複数のリクエストを送信すると、この値も変わります。Kubernetes ロードバランサーが別の pod を選択するのを待っている間に、最新のリクエストを返した pod を削除することで、プロセスを高速化できます。

$ kubectl delete pod gs-spring-boot-k8s-76477c6c99-5xdq8

要約

Spring Boot アプリケーションを Kubernetes で実行するには、start.spring.io にアクセスするだけで済みます。Spring Boot のゴールは常に、Java アプリケーションの構築と実行を可能な限り簡単にすることであり、アプリケーションの実行方法に関係なく、そのゴールを実現できるように努めています。Kubernetes を使用してクラウドネイティブアプリケーションを構築するには、Spring Boot の組み込みイメージビルダーを使用するイメージを作成し、Kubernetes プラットフォームの機能を活用するだけです。

コードを入手する