springboot-example of using spring cloud config client with kubernetes(k8s) configmap

Description

When we want to use kubernetes(k8s) configmap as our spring cloud app(or spring boot app) config server like this:

image-20201129173248794

And we want to have these features:

  • When the app starts in k8s, it can load the configurations from the k8s configmap resource.

  • If we change the k8s configmap, then the app should be reloaded automatically.

  • If the app has different profiles like developement or production, the configurations can be switched without changing the app itself ,this can be described as follows:

    image-20201129173959701

Environment

  • SpringBoot 2.3
  • Spring Cloud Config Server 2.2.3.RELEASE
  • SpringCloudVersion Hoxton.SR6
  • Kubernetes 1.19
  • Gradle 6.x
  • Docker 18.x

Example or tutorial of spring cloud app using kubernetes configmap as the config server

Note: The source code and configuration files of this example are uploaded to github, you can get the address at the bottom of this article.

Step 1: Setup the spring cloud app

We want to develop an app that listens to 8082 , and has a restful service like this:

image-20201129174657711

When we access the url : curl http://127.0.0.1:8082/greeting, it should return some messages.

The build.gradle of the app:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-config'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

As you can see, we add some dependencies as follows:

  • spring-cloud-starter-kubernetes-config is the dependency needed by spring cloud and kubernetes integerations
  • spring-boot-starter-web is needed by the restful service of the app

Then we add the code :

The main application entry point:

@RestController
public class GreetingController {
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @Autowired
    private MyConfig myConfig;

    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(),
                String.format(template, name+" "+myConfig.getMessage()));
    }
}

The greeting object:

public class Greeting {
    private long id;
    private String content;

		// getter and setters are omited
}


The greeting config object that would be used by the service:

@Configuration
@ConfigurationProperties(prefix = "myconfig")
public class MyConfig {
    private String message="default message";

		// getter and setter of message
}

The greeting restful service:

@RestController
public class GreetingController {
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @Autowired
    private MyConfig myConfig;

    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(),
                String.format(template, name+" "+myConfig.getMessage()));
    }
}

As the above code shown, the restful service would return a message from ‘myConfig’ , which is read by properties prefixed by ‘myconfig’.

Here is the bootstrap.properties needed by spring cloud config clients and kubernetes:

management:
  endpoint:
    restart.enabled : true
    health.enabled : true
    info.enabled : true

server:
  port: 8082

spring:
  application:
    name: app6
  cloud:
    kubernetes:
      reload:
        period: 15
        enabled: true
      config:
        enabled: true
        name: app6
        namespace: ns-bswen
        sources:
          - name: config-app6

The key properties are as follows:

  • spring.cloud.kubernetes.reload.enabled=true , this property tell spring cloud app that if the configmap changes, the app should reload the configrations automatically
  • spring.cloud.kubernetes.config.sources.name=config-app6, this property indicates that the spring cloud app would load the configurations from the kubernetes configmap named ‘config-app6’

Now the app codes are ready ,but if you start the app right now, it would cause some errors now:

> Task :app6:bootRun
2020-11-29 18:41:05.074  WARN 93760 --- [           main] o.s.c.k.KubernetesAutoConfiguration      : No namespace has been detected. Please specify KUBERNETES_NAMESPACE env var, or use a later kubernetes version (1.3 or later)

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

2020-11-29 18:41:15.263  WARN 93760 --- [           main] o.s.c.k.config.ConfigMapPropertySource   : Can't read configMap with name: [config-app6] in namespace:[ns-bswen]. Ignoring.

io.fabric8.kubernetes.client.KubernetesClientException: Operation: [get]  for kind: [ConfigMap]  with name: [config-app6]  in namespace: [ns-bswen]  failed.
	at io.fabric8.kubernetes.client.KubernetesClientException.launderThrowable(KubernetesClientException.java:64) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.KubernetesClientException.launderThrowable(KubernetesClientException.java:72) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.getMandatory(BaseOperation.java:229) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.get(BaseOperation.java:162) ~[kubernetes-client-4.4.1.jar:na]
	at org.springframework.cloud.kubernetes.config.ConfigMapPropertySource.getData(ConfigMapPropertySource.java:97) [spring-cloud-kubernetes-config-1.1.3.RELEASE.jar:1.1.3.RELEASE]
	at org.springframework.cloud.kubernetes.config.ConfigMapPropertySource.<init>(ConfigMapPropertySource.java:78) [spring-cloud-kubernetes-config-1.1.3.RELEASE.jar:1.1.3.RELEASE]
	at org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator.getMapPropertySourceForSingleConfigMap(ConfigMapPropertySourceLocator.java:96) [spring-cloud-kubernetes-config-1.1.3.RELEASE.jar:1.1.3.RELEASE]
	at org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator.lambda$locate$0(ConfigMapPropertySourceLocator.java:79) [spring-cloud-kubernetes-config-1.1.3.RELEASE.jar:1.1.3.RELEASE]
	at java.util.ArrayList.forEach(ArrayList.java:1249) ~[na:1.8.0_121]
	at org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator.locate(ConfigMapPropertySourceLocator.java:78) [spring-cloud-kubernetes-config-1.1.3.RELEASE.jar:1.1.3.RELEASE]
	at org.springframework.cloud.bootstrap.config.PropertySourceLocator.locateCollection(PropertySourceLocator.java:52) ~[spring-cloud-context-2.2.3.RELEASE.jar:2.2.3.RELEASE]
	at org.springframework.cloud.bootstrap.config.PropertySourceLocator.locateCollection(PropertySourceLocator.java:47) ~[spring-cloud-context-2.2.3.RELEASE.jar:2.2.3.RELEASE]
	at org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration.initialize(PropertySourceBootstrapConfiguration.java:98) ~[spring-cloud-context-2.2.3.RELEASE.jar:2.2.3.RELEASE]
	at org.springframework.boot.SpringApplication.applyInitializers(SpringApplication.java:626) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]
	at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:370) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]
	at com.bswen.app6.Main.main(Main.java:9) ~[main/:na]
Caused by: java.net.SocketTimeoutException: timeout
	at okio.Okio$4.newTimeoutException(Okio.java:232) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout.exit(AsyncTimeout.java:285) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:241) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.indexOf(RealBufferedSource.java:354) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:226) ~[okio-1.15.0.jar:na]
	io.fabric8.kubernetes.client.utils.ImpersonatorInterceptor.intercept(ImpersonatorInterceptor.java:68) ~[kubernetes-client-4.4.1.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) ~[okhttp-3.12.0.jar:na]
	at io.fabric8.kubernetes.client.utils.HttpClientUtils.lambda$createHttpClient$3(HttpClientUtils.java:110) ~[kubernetes-client-4.4.1.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall.execute(RealCall.java:92) ~[okhttp-3.12.0.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleResponse(OperationSupport.java:404) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleResponse(OperationSupport.java:365) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleGet(OperationSupport.java:330) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleGet(OperationSupport.java:311) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.handleGet(BaseOperation.java:810) ~[kubernetes-client-4.4.1.jar:na]
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.getMandatory(BaseOperation.java:218) ~[kubernetes-client-4.4.1.jar:na]
	... 16 common frames omitted
Caused by: java.net.SocketException: Socket closed
Caused by: java.net.SocketException: Socket closed

	at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_121]
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:171) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[na:1.8.0_121]
	at okio.Okio$2.read(Okio.java:140) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:237) ~[okio-1.15.0.jar:na]
	... 54 common frames omitted

2020-11-29 18:41:15.268  INFO 93760 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-configmap.config-app6.ns-bswen'}]
2020-11-29 18:41:15.276  INFO 93760 --- [           main] com.bswen.app6.Main                      : No active profile set, falling back to default profiles: default
2020-11-29 18:41:15.898  INFO 93760 --- [           main] o.s.cloud.context.scope.GenericScope     : BeanFactory id=44ca7d4b-412b-3e04-8a53-90616151854e
2020-11-29 18:41:16.200  INFO 93760 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8082 (http)
2020-11-29 18:41:16.216  INFO 93760 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-11-29 18:41:16.216  INFO 93760 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.37]
2020-11-29 18:41:16.319  INFO 93760 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-11-29 18:41:16.319  INFO 93760 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1029 ms
2020-11-29 18:41:16.630  INFO 93760 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-11-29 18:41:21.859  WARN 93760 --- [her.ais.com/...] i.f.k.c.d.i.WatchConnectionManager       : Exec Failure

java.net.SocketTimeoutException: timeout
	at okio.Okio$4.newTimeoutException(Okio.java:232) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout.exit(AsyncTimeout.java:285) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:241) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.indexOf(RealBufferedSource.java:354) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:226) ~[okio-1.15.0.jar:na]
	at okhttp3.internal.http1.Http1Codec.readHeaderLine(Http1Codec.java:215) ~[okhttp-
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) ~[okhttp-3.12.0.jar:na]
	at io.fabric8.kubernetes.client.utils.HttpClientUtils.lambda$createHttpClient$3(HttpClientUtils.java:110) ~[kubernetes-client-4.4.1.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall$AsyncCall.execute(RealCall.java:200) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) [okhttp-3.12.0.jar:na]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_121]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_121]
	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]
Caused by: java.net.SocketTimeoutException: Read timed out
Caused by: java.net.SocketTimeoutException: Read timed out

	at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_121]
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:171) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[na:1.8.0_121]
	at okio.Okio$2.read(Okio.java:140) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:237) ~[okio-1.15.0.jar:na]
	... 36 common frames omitted

2020-11-29 18:41:21.861 ERROR 93760 --- [           main] .r.EventBasedConfigurationChangeDetector : Error while establishing a connection to watch config maps: configuration may remain stale

io.fabric8.kubernetes.client.KubernetesClientException: Failed to start websocket
	at io.fabric8.kubernetes.client.dsl.internal.WatchConnectionManager$1.onFailure(WatchConnectionManager.java:209) ~[kubernetes-client-4.4.1.jar:na]
	at okhttp3.internal.ws.RealWebSocket.failWebSocket(RealWebSocket.java:571) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.ws.RealWebSocket$2.onFailure(RealWebSocket.java:221) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall$AsyncCall.execute(RealCall.java:215) ~[okhttp-3.12.0.jar:na]
	at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) ~[okhttp-3.12.0.jar:na]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_121]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_121]
	at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_121]
Caused by: java.net.SocketTimeoutException: timeout
Caused by: java.net.SocketTimeoutException: timeout

	at okio.Okio$4.newTimeoutException(Okio.java:232) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout.exit(AsyncTimeout.java:285) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:241) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.indexOf(RealBufferedSource.java:354) ~[okio-1.15.0.jar:na]
	at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:226) ~[okio-1.15.0.jar:na]
Exception in thread "OkHttp Dispatcher" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask@76a5c602 rejected from java.util.concurrent.ScheduledThreadPoolExecutor@6c3ad7e7[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
	at okhttp3.internal.http1.Http1Codec.readHeaderLine(Http1Codec.java:215) ~[okhttp-3.12.0.jar:na]
	at 
	at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254) ~[okhttp-3.12.0.jar:na]
	at okhttp3.RealCall$AsyncCall.execute(RealCall.java:200) ~[okhttp-3.12.0.jar:na]
	... 4 common frames omitted
Caused by: java.net.SocketTimeoutException: Read timed out
Caused by: java.net.SocketTimeoutException: Read timed out

	at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_121]
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:171) ~[na:1.8.0_121]
	at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[na:1.8.0_121]
	at okio.Okio$2.read(Okio.java:140) ~[okio-1.15.0.jar:na]
	at okio.AsyncTimeout$2.read(AsyncTimeout.java:237) ~[okio-1.15.0.jar:na]
	... 36 common frames omitted

2020-11-29 18:41:21.871  INFO 93760 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2020-11-29 18:41:21.935  INFO 93760 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8082 (http) with context path ''
2020-11-29 18:41:21.952  INFO 93760 --- [           main] com.bswen.app6.Main                      : Started Main in 18.174 seconds (JVM running for 18.638)

Because we do not provide the app with kubernetes environment and the configmap to load, so the app should not start correctly.

Step 2: Containerize(dockerize) the app

Build the app at first:

gradlew build

Now, we should build the docker image of the app, we use the Dockerfile as follows:

FROM openjdk:8-jdk-alpine

ENV APPROOT="/opt/app6"
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib ${APPROOT}/lib
COPY ${DEPENDENCY}/META-INF ${APPROOT}/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes ${APPROOT}

ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-cp","/opt/app6:/opt/app6/lib/*","-Droot.dir=/opt/app6","com.bswen.app6.Main"]
EXPOSE 8082

The above Dockerfile depends on the gradle builds of the app, if you use maven, you should change the directory of the ‘DEPENDENCY’.

Then run docker build to build the docker image of our spring cloud app:

docker build -t app6:latest -f Dockerfile .

Then run docker ps to verify that the docker images are created correctly:

➜  bswen git:(master) ✗ docker images
REPOSITORY                              TAG                 IMAGE ID            CREATED             SIZE
app6   latest                           4aaf0c8cdda5        1 hours ago         142MB
openjdk                                 8-jdk-alpine        a3562aa0b991        18 months ago       105MB

Then we need to push the docker image to the docker image repository as follows:

docker push app6:latest

Step 3: Deploy the spring cloud app to kubernetes

We create yamls to deploy the app to kubernetes as follows,

  1. we create the RBAC yaml to grant the configmap read permission to our app
# create the service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reader
  namespace: ns-bswen
---
# create the role to grant access to configmaps
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: ns-bswen
  name: role-api-reader
rules:
  - apiGroups: [""] # "" indicates the core API group
    resources: ["pods","configmaps"]
    verbs: ["get", "watch", "list"]
---
# bind the role and the service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rolebinding-api-reader
  namespace: ns-bswen
subjects:
  - kind: ServiceAccount
    name: api-reader # Name is case sensitive
    namespace: ns-bswen
roleRef:
  kind: Role #this must be Role or ClusterRole
  name: role-api-reader # this must match the name of the Role or ClusterRole you wish to bind to
  apiGroup: rbac.authorization.k8s.io

Then we can use the service account in our kubernetes deployment yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-app6
  namespace: ns-bswen
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app6
  template:
    metadata:
      labels:
        app: app6
    spec:
      serviceAccountName: api-reader  # here is the key point
      imagePullSecrets:
        - name: secret-harbor
      containers:
        - image: app6:latest
          name: app6
          ports:
            - containerPort: 8082
              name: app6-port
        - name: busybox
          image: busybox
          command: ["sleep"]
          args: ["1000000000"]

As the above yaml shown, we added two containers into the pod, one is ‘app6’, the other is ‘busybox’, which is used to debug and test the app.

Then we create the configmap that would be loaded by our app:

apiVersion: v1
kind: ConfigMap
metadata:
  name: config-app6
  namespace: ns-bswen
data:
  application.yml: |-
    myconfig:
      message: "from k8s configmap"

Then apply all the above yamls via kubectl as follows:

kubectl apply k8s/*.yaml

Verify that the deployment in kubernetes is correct:

➜  bswen git:(master) ✗ k get deployments -n ns-bswen
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment-app6          1/1     1            1           24h

Step 4: Test the restful service in kubernetes

Then we can test the application in kubernetes as follows:

➜  bswen git:(master) ✗ k exec -it deployment-app6-57bdb6fb8-47rtw -n ns-bswen -c busybox -- sh
/home # curl http://127.0.0.1:8082/greeting
{"id":1,"content":"Hello, World from k8s configmap"}
/home #
/home #

As you can see, the kubernetes(k8s) configmap works!

Now we test the auto-reload feature, we change the configmap as follows:

apiVersion: v1
kind: ConfigMap
metadata:
  name: config-app6
  namespace: ns-bswen
data:
  application.yml: |-
    myconfig:
      message: "from k8s configmap222"

Then we got this log message in the kubernetes pod:

2020-11-29 12:57:29.950  INFO 1 --- [//10.43.0.1/...] .r.EventBasedConfigurationChangeDetector : Detected change in config maps
2020-11-29 12:57:29.950  INFO 1 --- [//10.43.0.1/...] .r.EventBasedConfigurationChangeDetector : Reloading using strategy: REFRESH
2020-11-29 12:57:30.294  INFO 1 --- [//10.43.0.1/...] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-configmap.config-app6.ns-bswen'}]
2020-11-29 12:57:30.307  INFO 1 --- [//10.43.0.1/...] o.s.boot.SpringApplication               : Started application in 0.354 seconds (JVM running for 125.533)

If we test the restful service again, we get this:

➜  bswen git:(master) ✗ k exec -it deployment-app6-57bdb6fb8-47rtw -n ns-bswen -c busybox -- sh
/home # curl http://127.0.0.1:8082/greeting
{"id":3,"content":"Hello, World from k8s configmap222"}
/home #

Step 5: Switch the profiles without changing the app

Now we want to change the profiles in kubernetes environment without changing the app, how to achieve this feature?

  1. We need to define different profile properties in the configmap as follows:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: config-app6
      namespace: ns-bswen
    data:
      application.yml: |-
        myconfig:
          message: "from k8s configmap"
        ---
        spring:
          profiles: dev
        myconfig:
          message: "from dev configmap"
        ---
        spring:
          profiles: prod
        myconfig:
          message: "from prod configmap"
    

    Here we define two profiles: dev and prod, each with the message property overridden.

  2. We need to switch to specific profile for the app

    To achieve this, we need to change the deployment yaml of the app:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: deployment-app6
      namespace: ns-bswen
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: app6
      template:
        metadata:
          labels:
            app: app6
        spec:
          serviceAccountName: api-reader  # here is the key point
          imagePullSecrets:
            - name: secret-harbor
          containers:
            - image: app6:latest
              name: app6
              env:
                - name: SPRING_PROFILES_ACTIVE
                  value: "dev"
              ports:
                - containerPort: 8082
                  name: app6-port
            - name: busybox
              image: busybox
              command: ["sleep"]
              args: ["1000000000"]
    

    Pay attention to this part:

              env:
                - name: SPRING_PROFILES_ACTIVE
                  value: "dev"
    

    We add an environment variable ‘SPRING_PROFILES_ACTIVE’ to the app, then we apply this change to kubernetes:

    kubectl apply -f k8s/*.yaml
    

    Then we re-test the app:

    ➜  bswen git:(master) ✗ k exec -it deployment-app6-57bdb6fb8-47rt1 -n ns-bswen -c busybox -- sh
    /home # curl http://127.0.0.1:8082/greeting
    {"id":1,"content":"Hello, World from dev configmap"}
    /home #
    

    It works!

All the example code and config files can be found in this github project.