Abstruct

KubernetesのPod上で動作するSpringBootアプリケーションからConfigMapを取得しに行くことができたので、その方法をまとめておきます。

Pod上にマウントされたConfigMapが更新されるのを待つのではなく、直接取得しに行く方法です。

追記:fabric8io/kubernetes-client を使用した方がConfigMapの変更イベントをトリガーにできるため、より簡単に実現できます。
参考:kubernetes-client/WatchExample.java at master · fabric8io/kubernetes-client

Environment

  • minikube
    • version
    1
    2
    
    minikube version: v1.16.0
    commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1
    
  • docker
    • version
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    Client: Docker Engine - Community
    Cloud integration: 1.0.4
    Version:           20.10.0
    API version:       1.41
    Go version:        go1.13.15
    Git commit:        7287ab3
    Built:             Tue Dec  8 18:55:43 2020
    OS/Arch:           darwin/amd64
    Context:           default
    Experimental:      true
    
    Server: Docker Engine - Community
    Engine:
    Version:          20.10.0
    API version:      1.41 (minimum version 1.12)
    Go version:       go1.13.15
    Git commit:       eeddea2
    Built:            Tue Dec  8 18:58:04 2020
    OS/Arch:          linux/amd64
    Experimental:     false
    containerd:
    Version:          v1.4.3
    GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
    runc:
    Version:          1.0.0-rc92
    GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
    docker-init:
    Version:          0.19.0
    GitCommit:        de40ad0
    

環境構築は以下のコマンド。

1
2
minikube start
eval $(minikube docker-env)

eval $(minikube docker-env) の必要性については、こちらのブログがわかりやすかったです。

What to create

  • ConfigMap
    • 読み取り対象のConfigMap
    • 公式サイト(ConfigMap | Kubernetes)にあるサンプルを利用する。
  • ServiceAccount
    • Pod上からKubernetesのリソースにアクセスするためのアカウント。
  • RoleBinding
  • DockerImage
    • SpringBootApplicationを配置/実行するイメージ
  • Pod
    • 作成したDockerImageを読み込むPod
  • SpringBootApplication
    • ConfigMapにアクセスするプログラム。

ConfigMap

game-demo-configmap.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"
  game.properties: |
    enemy.types: aliens,monsters
    player.maximum-lives: 5    
  user-interface.properties: |
    color.good: purple
    color.bad: yellow
    allow.textmode: true    

ConfigMapの作成方法は以下。

1
kubectl apply -f game-demo-configmap.yaml

ServiceAccount

service-account.yaml

1
2
3
4
5
apiVersion: v1
kind: ServiceAccount
metadata:
  name: configmap-access-sa
automountServiceAccountToken: true

ServiceAccountの作成方法は以下。

1
kubectl apply -f service-account.yaml

RoleBinding

role-binding.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: configmap-access-sa-view
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: ServiceAccount
  name: configmap-access-sa
  namespace: default

Roleのデフォルトで用意されているもの(cluster-adminviewなど)については、Using RBAC Authorization | Kubernetes を参照するとわかりやすいです。

RoleBindingの作成方法は以下。

1
kubectl apply -f ./role-binding.yaml

DockerImage

1
2
3
4
FROM openjdk:11-jre
ARG JAR_FILE=./build/libs/app.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "./app.jar"]S

DockerImageの作成方法は以下。

1
docker build . -t game.example/demo-game

Pod

configmap-demo-pod.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: Pod
metadata:
  name: configmap-demo-pod
spec:
  serviceAccountName: configmap-access-sa
  automountServiceAccountToken: true
  containers:
    - name: demo
      image: game.example/demo-game:latest
      imagePullPolicy: IfNotPresent
      env:
        - name: PLAYER_INITIAL_LIVES
          valueFrom:
            configMapKeyRef:
              name: game-demo
              key: player_initial_lives 
        - name: UI_PROPERTIES_FILE_NAME
          valueFrom:
            configMapKeyRef:
              name: game-demo
              key: ui_properties_file_name
      volumeMounts:
      - name: config
        mountPath: "/config"
        readOnly: true
  volumes:
    - name: config
      configMap:
        name: game-demo
        items:
        - key: "game.properties"
          path: "game.properties"
        - key: "user-interface.properties"
          path: "user-interface.properties"

Podの作成方法は以下。

1
kubectl apply -f ./configmap-demo-pod.yaml

ServiceAccount作成時にできているSecretがマウントされていることを確認すること。

この設定がないと、io.kubernetes.client.ApiException: Forbidden at K8S pod · Issue #542 · kubernetes-client/java のエラーとなり、kubernetesのリソースにアクセスできない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
takuto-n@MacBook-Air pod-read-configmap % kubectl get pods/configmap-demo-pod -o yaml
# 省略
  serviceAccount: configmap-access-sa
  serviceAccountName: configmap-access-sa
# 省略
    volumeMounts:
    - mountPath: /config
      name: config
      readOnly: true
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: configmap-access-sa-token-2vxlm
      readOnly: true

SpringBootApplication

build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
plugins {
	id 'org.springframework.boot' version '2.3.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}

	// for k8s
	implementation 'io.kubernetes:client-java:11.0.0'
}

test {
	useJUnitPlatform()
}

SimpleRestControllerApplication.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.simplerestcontroller;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SimpleRestControllerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SimpleRestControllerApplication.class, args);
    }

}

ConfigMapWatchScheduler.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.simplerestcontroller;

import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.util.ClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class ConfigMapWatchScheduler {

    private Map<String, String> configMapData;
    private CoreV1Api coreV1Api;
    private String configMapNamespace = "default";

    @PostConstruct
    private void init() throws IOException {
        // coreV1Apiの準備
        var apiClient = ClientBuilder.cluster().build();
        var httpClient = apiClient.getHttpClient().newBuilder().readTimeout(0, TimeUnit.SECONDS).build();
        apiClient.setHttpClient(httpClient);
        coreV1Api = new CoreV1Api(apiClient);

        // configMapDataの準備
        configMapData = new HashMap<String, String>();
    }

    @Scheduled(cron = "*/1 * * * * *", zone = "Asia/Tokyo")
    public void watch() throws ApiException {
        List<V1ConfigMap> list = coreV1Api.listNamespacedConfigMap(configMapNamespace, null, null, null, null, null, null, null, null, null, null).getItems();
        for(V1ConfigMap data : list){
            for(var key : data.getData().keySet()){
                var value = data.getData().get(key);
                // configMapDataの更新
                if(!configMapData.containsKey(key)){
                    configMapData.put(key, value);
                    log.info("key = {}, value = {}", key, value);
                }else{
                    if(!configMapData.get(key).equals(value)){
                        configMapData.put(key, value);
                        log.info("key = {}, value = {}", key, value);
                    }
                }
            }
        }
    }
}

Execution Result

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
takuto-n@MacBook-Air pod-read-configmap % kubectl logs configmap-demo-pod                      

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2021-01-07 08:52:08.068  INFO 1 --- [           main] c.e.s.SimpleRestControllerApplication    : Starting SimpleRestControllerApplication on configmap-demo-pod with PID 1 (/app.jar started by root in /)
2021-01-07 08:52:08.079  INFO 1 --- [           main] c.e.s.SimpleRestControllerApplication    : No active profile set, falling back to default profiles: default
2021-01-07 08:52:12.385  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-01-07 08:52:12.424  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-01-07 08:52:12.424  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.35]
2021-01-07 08:52:12.702  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-01-07 08:52:12.703  INFO 1 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 4403 ms
2021-01-07 08:52:15.437  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-01-07 08:52:16.257  INFO 1 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
2021-01-07 08:52:16.372  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-07 08:52:16.504  INFO 1 --- [           main] c.e.s.SimpleRestControllerApplication    : Started SimpleRestControllerApplication in 10.909 seconds (JVM running for 13.051)
2021-01-07 08:52:18.387  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = game.properties, value = enemy.types: aliens,monsters
player.maximum-lives: 10

2021-01-07 08:52:18.391  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = player_initial_lives, value = 3
2021-01-07 08:52:18.391  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = ui_properties_file_name, value = user-interface.properties
2021-01-07 08:52:18.392  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = user-interface.properties, value = color.good: purple
color.bad: yellow
allow.textmode: true

2021-01-07 08:52:18.394  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = ca.crt, value = -----BEGIN CERTIFICATE-----
MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
a3ViZUNBMB4XDTIwMDcxMjAzMTQzMFoXDTMwMDcxMTAzMTQzMFowFTETMBEGA1UE
AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+B
mkYO8z5hN/ijMV6RYeaVGwM78QL3ciI1t3TctVzYFxEMgX+w8r9B5WDzmRNSpfPR
Q5Rtd5uu7qZilCB6C2Hl6y6PJRLm+hkT6qb6SYJ+jOafxnxhqfLuSsNPRk7xcayT
/pr78yQk49PtlcgaH5TjbkH5CrwoV5DtHIAyn5Fy+g9m0mHlJQruwlgFFvdR5Ub0
JeO93gzbrcm1nni6NbOo3rn53lqlc8lrVzC5m2vYiV/cVELPS/RedRDhf9WVjVVN
VStC0oz1xQer1gRA8Tj0h7ZrqtDjN+hynRLrx3PwQKhHQDiPk6bDkBOtVBI2LJu9
X4dTy3R9ebAxakWBspcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW
MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQAbvw2bBmcs2cLag0TXyEPSBXp518+BqVh2pHZ8VkAGirxR7JYr
x9hTI24rVgsAorH/0GGvH8Qp6rjYEKfK08iz2RTRNcgNWSqq3XHeaeylnFNKT3So
25aNXUe2EUhsQw5/8Sf3R2LaC1edUT1KsxpZGeSf8dhubHGEIBe5VWM6QOyzZR04
ud78fUkOR/XtaUNHzQYEgcKn3kH75w0hmlSon4x5jEuTuSaVyYA51uHMEWM6EC77
s5FWILDpEEZcaTsuK/BwBCzIYFdpbVlH7EgFjO+X9CfKMF7NZOvTYNqdS9Cs6v0o
zmIxgDYvY/ObkPHoC7rwV3Qa5P9SD+r4t0v5
-----END CERTIFICATE-----
# ConfigMapのplayer.maximum-livesの値を「5」に変更
2021-01-07 08:54:04.058  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = game.properties, value = enemy.types: aliens,monsters
player.maximum-lives: 5

# ConfigMapのplayer.maximum-livesの値を「10」に変更
2021-01-07 08:54:28.014  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = game.properties, value = enemy.types: aliens,monsters
player.maximum-lives: 10

# ConfigMapのplayer.maximum-livesの値を「5」に変更
2021-01-07 08:55:08.029  INFO 1 --- [   scheduling-1] c.e.s.ConfigMapWatchScheduler            : key = game.properties, value = enemy.types: aliens,monsters
player.maximum-lives: 5

Reference