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:
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:
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:
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.concur[email protected][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,
- 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?
-
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.
-
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.