How to read spring boot configuration file in Kubernetes deployment - spring-boot

Im new in Kubernetes and having a hard time making to read application.properties in the deployment. I have attached our ConfigMap as a mounted volume under the /config path.
This is my deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: 34343434.dkr.ecr.asia-2.amazonaws.com/myapp:latest
ports:
- containerPort: 80
volumeMounts:
- name: application-properties
mountPath: /config
volumes:
- name: application-properties
configMap:
name: application-properties
I have created configmap using kubectl command from a file that is located in my local computer.
kubectl create configmap application-properties -–from-file=/users/me/application.properties
Now the issue is the application.property file which i am setting it using the kubectl configmap is not getting picked up. Can you help me on this?

Based on the discussion, the issue was the configmap, instead of the property file, it was rendered as a string in the configmap.
kubectl get configmap application-properties -o yaml
>shows the contents but with all in oneline format. separated by \n
Converting it to YAML application.yml did the trick.

Related

Spring Boot - read container environment variables in properties file

I use:
Spring Boot
Microservices (containerized)
Docker
Kubernetes
My case is as follows:
I have to generate link:
https://dev-myapp.com OR https://qa-myapp.com
depending on the environment in which my service is running (DEV, QA)
Depending on the environment (DEV, QA). I have one Spring profile BUT under this profile my app can run in kubernetes on 2 types of environment: DEV or QA. I want to generate proper link - read it from my properties file:
#Value("${email.body}")
private String emailBody;
application.yaml:
email:
body: Click on the following URL: ${ENVIRONMENT_URL:}/edge/invitation?code={0}&email={1}
DEVOPS(Kubernetes):
Manifest in workloads folder (DEV branch, the same for qa branch nut this time with https://qa-myapp.com):
apiVersion: v1
kind: Service
...
...
apiVersion: apps/v1
kind: Deployment
...
...
containers:
env:
- name: ENVIRONMENT_URL
value: https://dev-myapp.com
So is i possible to read that value from kubernetes container in my Spring properties file? I want to get email.body property depending on the container my service is running on.
Yes this is possible and have corrected the syntax of the yaml
apiVersion: v1
kind: Service
...
...
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
command: ["/bin/sh", "-c", "env | grep ENVIRONMENT_URL"]
env:
- name: ENVIRONMENT_URL
value: https://myapp.com. #Indedntation Changed
ports:
- containerPort: 80

How to properly configure the environment in kubernetes cluster?

I have a spring boot application with two profiles, dev and prod, my docker file is:
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-Dspring.profiles.active=dev","-cp","app:app/lib/*","com.my.Application"]
please not that, when building the image, I specify the entrypoint as command line argument.
This is the containers section of my kubernetes deployment where I use this image:
containers:
- name: myapp
image: myregistry.azurecr.io/myapp:0.1.7
imagePullPolicy: "Always"
ports:
- containerPort: 8080
name: myapp
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
timeoutSeconds: 3
periodSeconds: 20
failureThreshold: 3
It works but has a major flaw: how can I now switch to the production environment without rebuilding the image?
The best would be to remove that ENTRYPOINT in my docker file and give this configuration in my kubernetes yml so that I could always use the same image...is this possible?
edit: I saw that there is a lifecycle istruction but note that I have a readiness probe based on the spring boot's actuator. It would always fail if I used this construct.
You can override an image's ENTRYPOINT by using the command property of a Kubernetes Pod spec. Likewise, you could override CMD by using the args property (also see the documentation):
containers:
- name: myapp
image: myregistry.azurecr.io/myapp:0.1.7
imagePullPolicy: "Always"
command: ["java","-Dspring.profiles.active=prod","-cp","app:app/lib/*","com.my.Application"]
ports:
- containerPort: 8080
name: myapp
Alternatively, to provide a higher level of abstraction, you might write your own entrypoint script that reads the application profile from an environment variable:
#!/bin/sh
PROFILE="${APPLICATION_CONTEXT:-dev}"
exec java "-Dspring.profiles.active=$PROFILE" -cp app:app/lib/* com.my.Application
Then, you could simply pass that environment variable into your pod:
containers:
- name: myapp
image: myregistry.azurecr.io/myapp:0.1.7
imagePullPolicy: "Always"
env:
- name: APPLICATION_CONTEXT
value: prod
ports:
- containerPort: 8080
name: myapp
Rather than putting spring.profiles.active in dockerfile in the entrypoint.
Make use of configmaps and application.properties.
Your ENTRYPOINT in dockerfile should look like:
ENTRYPOINT ["java","-cp","app:app/lib/*","com.my.Application","--spring.config.additional-location=/config/application-dev.properties"]
Create a configmap that acts as application.properties for your springboot application
---
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: flow
data:
application-dev.properties: |
spring.application.name=myapp
server.port=8080
spring.profiles.active=dev
NOTE: Here we have specified spring.profiles.active.
In containers section of my kubernetes deployment mount the configmap inside container that will act as application.properties.
containers:
- name: myapp
image: myregistry.azurecr.io/myapp:0.1.7
imagePullPolicy: "Always"
command: ["java","-cp","app:app/lib/*","com.my.Application","--spring.config.additional-location=/config/application-dev.properties"]
ports:
- containerPort: 8080
name: myapp
volumeMounts:
- name: myapp-application-config
mountPath: "/config"
readOnly: true
volumes:
- name: myapp-application-config
configMap:
name: myapp-config
items:
- key: application-dev.properties
path: application-dev.properties
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
timeoutSeconds: 3
periodSeconds: 20
failureThreshold: 3
NOTE: --spring.config.additional-location points to location of application.properties that we created in configmaps.
So making use of configmaps and application.properties one can override any configuration of your application wihtout rebuilding the image.
If you want to add a new config or update value of existing config, just make appropriate changes in configmap and kubectl apply it. Then scale down and scale up your application pod, to bring the new config in action.
Hope this helps.
There are many many ways to set Spring configuration values. With some rules, you can use ordinary environment variables to specify individual property values. You might see if you can use this instead of having a separate Spring profile control.
Using environment variables has two advantages here: it means you (or your DevOps team) can change deploy-time settings without recompiling the application; and if you're using a deployment manager like Helm where some details like hostnames are intrinsically unpredictable, this lets you specify values that can't be known until deploy time.
For example, let's say you have a Redis dependency:
cache:
redis:
url: redis://localhost:6379/0
You could override this at deploy time by setting
containers:
- name: myapp
env:
- name: CACHE_REDIS_URL
value: "redis://myapp-redis.default.svc.cluster.local:6379/0"
One way to do this is using spring cloud Kubernetes as described here
https://docs.spring.io/spring-cloud-kubernetes/docs/current/reference/html/index.html#configmap-propertysource
You can define your profiles in a configmap like below
kind: ConfigMap
apiVersion: v1
metadata:
name: demo
data:
application.yml: |-
greeting:
message: Say Hello to the World
farewell:
message: Say Goodbye
---
spring:
profiles: development
greeting:
message: Say Hello to the Developers
farewell:
message: Say Goodbye to the Developers
---
spring:
profiles: production
greeting:
message: Say Hello to the Ops
And can then select the desired profile by passing an environment variable in your Kubernetes deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-name
labels:
app: deployment-name
spec:
replicas: 1
selector:
matchLabels:
app: deployment-name
template:
metadata:
labels:
app: deployment-name
spec:
containers:
- name: container-name
image: your-image
env:
- name: SPRING_PROFILES_ACTIVE
value: "development"

Start sprinboot in minkube with custom context url as environnement variable

I have a springboot app which I want to deploy on Kubernetes (I'm using minikube) with a custom context path taken from the environment variables.
I've compiled an app.war file. exported an environment variable in Linux as follow:
export SERVER_SERVLET_CONTEXT_PATH=/app
And then started my app on my machine as follow:
java -jar app.war --server.servlet.context-path=$(printenv CONTEXT_PATH)
and it works as expected, I can access my app throw browser using the url localhost:8080/app/
I want to achieve the same thing on minikube so I prepared those config files:
Dockerfile:
FROM openjdk:8
ADD app.war app.war
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.war", "--server.servlet.context-path=$(printenv CONTEXT_PATH)"]
deployment config file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: esse-deployment-1
labels:
app: esse-1
spec:
replicas: 1
selector:
matchLabels:
app: esse-1
template:
metadata:
labels:
app: esse-1
spec:
containers:
- image: mysql:5.7
name: esse-datasource
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- image: esse-application
name: esse-app
imagePullPolicy: Never
ports:
- containerPort: 8080
env:
- name: server.servlet.context-path
value: /esse-1
volumes:
- name: esse-1-mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-persistent-storage-claim
---
apiVersion: v1
kind: Service
metadata:
name: esse-service-1
labels:
app: esse-1
spec:
selector:
app: esse-1
ports:
- protocol: TCP
port: 8080
targetPort: 8080
type: NodePort
However, the java container inside the pod fails to start and here's the exception is thrown by spring:
Initialization of bean failed; nested exception is
java.lang.IllegalArgumentException: ContextPath must start with '/'
and not end with '/'
Make use of configmaps.
The configmap will holds application.properties of your springboot application.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: esse-config
data:
application-dev.properties: |
spring.application.name=my-esse-service
server.port=8080
server.servlet.context-path=/esse-1
NOTE: server.servlet.context-path=/esse-1 will override context-path of your springboot application.
Now refer this configmap in your deployment yaml.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: esse-deployment-1
labels:
app: esse-1
spec:
replicas: 1
selector:
matchLabels:
app: esse-1
template:
metadata:
labels:
app: esse-1
spec:
containers:
- image: mysql:5.7
name: esse-datasource
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- image: esse-application
name: esse-app
imagePullPolicy: Never
command: [ "java", "-jar", "app.war", "--spring.config.additional-location=/config/application-dev.properties" ]
ports:
- containerPort: 8080
volumeMounts:
- name: esse-application-config
mountPath: "/config"
readOnly: true
volumes:
- name: esse-application-config
configMap:
name: esse-config
items:
- key: application-dev.properties
path: application-dev.properties
- name: esse-1-mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-persistent-storage-claim
NOTE: Here we are mounting configmap inside your springboot application container at /config folder. Also --spring.config.additional-location=/config/application-dev.properties is pointing to the application.properties config file.
In future if you want to add any new config or update the value of existing config that just make the change in configmap and kubectl apply it. Then to reflect those new config changes, just scale down and scale up the deployment.
Hope this helps.
Finally, I found a solution.
I configured my application to startup with a value for the context path taken from the environment variables by adding this line inside my application.properties:
server.servlet.context-path=${ESSE_APPLICATION_CONTEXT}
And the rest remains as it was, means I'm giving the value of the variable ESSE_APPLICATION_CONTEXT throw the config
env:
- name: ESSE_APPLICATION_CONTEXT
value: /esse-1
And then starting the application without the --server.servlet.context-path parameeter, which means like that:
java -jar app.war
NOTE: as pointed by #mchawre's answer, it's also possible to make use of ConfigMap as documented in Kubernetes docs.
Looks like what you want is SERVER_SERVLET_CONTEXT_PATH variable defined in your container spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: esse-deployment-1
labels:
app: esse-1
spec:
replicas: 1
selector:
matchLabels:
app: esse-1
template:
metadata:
labels:
app: esse-1
spec:
containers:
- image: mysql:5.7
name: esse-datasource
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- image: esse-application
name: esse-app
imagePullPolicy: Never
ports:
- containerPort: 8080
env:
- name: SERVER_SERVLET_CONTEXT_PATH <== HERE
value: /esse-1
volumes:
- name: esse-1-mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-persistent-storage-claim
Note that in your Pod spec you are using /esse-1 while on your local setup you have /app

ConfigMap data (yml format) - Kubernetes

I have an application.yml (Spring) file, which has almost 70 fields, want to move those fields to ConfigMap.
In the process of setup ConfigMap, have realized all the 70 fields has be flatened example : webservice.endpoint.transferfund
It's gonna be a painful task to convert all the 70 fields as flat, is there any alternative.
Please suggest.
Below Config is working:
apiVersion: v1
kind: ConfigMap
metadata:
name: configmapname
namespace: default
data:
webservice.endpoint.transferfund: http://www.customer-service.app/api/tf
webservice.endpoint.getbalance: http://www.customer-service.app/api/balance
webservice.endpoint.customerinfo: http://www.customer-service.app/api/customerinfo
Below config is not working, tried it as yml format.
apiVersion: v1
kind: ConfigMap
metadata:
name: configmapname
namespace: default
data:
application.yaml: |-
webservice:
endpoint:
transferfund: http://www.customer-service.app/api/tf
getbalance: http://www.customer-service.app/api/balance
customerinfo: http://www.customer-service.app/api/customerinfo
in src/main/resources/application.yml have below fields to access ConfigMap keys:
webservice:
endpoint:
transferfund: ${webservice.endpoint.transferfund}
getbalance: ${webservice.endpoint.getbalance}
customerinfo: ${webservice.endpoint.customerinfo}
Updated:
ConfigMap Description:
C:\Users\deskktop>kubectl describe configmap configmapname
Name: configmapname
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
application.yaml:
----
webservice:
endpoint:
transferfund: http://www.customer-service.app/api/tf
getbalance: http://www.customer-service.app/api/balance
customerinfo: http://www.customer-service.app/api/customerinfo
Events: <none>
Deployment script: (configMapRef name provided as configmap name as shown above)
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: configmap-sample
spec:
replicas: 1
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: configmap-sample
spec:
containers:
- name: configmap-sample
image: <<image>>
ports:
- name: http-api
containerPort: 9000
envFrom:
- configMapRef:
name: configmapname
resources:
limits:
memory: 1Gi
requests:
memory: 768Mi
env:
- name: JVM_OPTS
value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Xms768M"
A ConfigMap is a dictionary of configuration settings. It consists of key-value pairs of strings. Kubernetes then adds those values to your containers.
In your case you have to make them flat, because Kubernetes will not understand them.
You can read in the documentation about Creating ConfigMap that:
kubectl create configmap <map-name> <data-source>
where is the name you want to assign to the ConfigMap and is the directory, file, or literal value to draw the data from.
The data source corresponds to a key-value pair in the ConfigMap, where
key = the file name or the key you provided on the command line, and
value = the file contents or the literal value you provided on the command line.
You can use kubectl describe or kubectl get to retrieve information about a ConfigMap.
EDIT
You could create a ConfigMap from a file with defined key.
Define the key to use when creating a ConfigMap from a file
Syntax might look like this:
kubectl create configmap my_configmap --from-file=<my-key-name>=<path-to-file>
And the ConfigMap migh look like the following:
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: 2019-07-03T18:54:22Z
name: my_configmap
namespace: default
resourceVersion: "530"
selfLink: /api/v1/namespaces/default/configmaps/my_configmap
uid: 05f8da22-d671-11e5-8cd0-68f728db1985
data:
<my-key-name>: |
key=value
key=value
key=value
key=value
Also I was able to find Create Kubernetes ConfigMaps from configuration files.
Functionality
The projector can:
Take raw files and stuff them into a ConfigMap
Glob files in your config repo, and stuff ALL of them in your configmap
Extract fields from your structured data (yaml/json)
Create new structured outputs from a subset of a yaml/json source by pulling out some fields and dropping others
Translate back and forth between JSON and YAML (convert a YAML source to a JSON output, etc)
Support for extracting complex fields like objects+arrays from sources, and not just scalars!
You need to mount the ConfigMap as Volume. Otherwise the content would live in environment variables. The example i post here is from https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command: [ "/bin/sh", "-c", "ls /etc/config/" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
# Provide the name of the ConfigMap containing the files you want
# to add to the container
name: special-config
restartPolicy: Never
You mentioned, you're using the application.yaml in context of a Spring project. So if you don't care whether you use .yaml or .property configuration-files, you can just use property-files because configMap generation supports them. It works with the --from-env-file flag:
kubectl create configmap configmapname --from-env-file application.properties
So in your deployment-file you can directly access the keys:
...
env:
- KEYNAME
valueFrom:
configMapKeyRef:
name: configmapname
key: KeyInPropertiesFile

Minikube springboot receive request and doesn't respond correctly

I'm beginning with kubernetes and docker and facing an issue.
Deployed a springboot app on minikube after converting it to docker image (using minikube's docker)... the app is online and receiving request so well as you can see in the below screenshots, but doesn't reply as expected.
For example, when i deploy the app normally (on my computer like usually) everything works well i can go on all html pages etc, but once deployed inside minikube it doesn't reply correctly. (all working part is the receiving of favicon of spring)
YAMLs used to deploy the app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: esse-deployment-1
labels:
app: esse
spec:
replicas: 1
selector:
matchLabels:
app: esse-1
template:
metadata:
labels:
app: esse-1
spec:
containers:
- image: mysql:5.7
name: esse-datasource
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: esse_password
- image: esse_application
name: esse-app-1
imagePullPolicy: Never
ports:
- containerPort: 8080
volumes:
- name: esse-1-mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-persistent-storage-claim
---
apiVersion: v1
kind: Service
metadata:
name: esse-service-1
spec:
selector:
app: esse-1
ports:
- protocol: TCP
port: 8080
type: NodePort
----
kind: PersistentVolume
apiVersion: v1
metadata:
name: mysql-persistent-storage
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/home/docker/data"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-persistent-storage-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Mi
Docker file to contruct image:
FROM openjdk:8
ADD ESSE_Application.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
I can see you have .yml files to define the deployment and the service, but I can see no "Ingress". A .yml file of kind: Route is needed in order to tell kubernetes that there should be an external url pointing to your service; a minimal Ingress resource example:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: test-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
**Please don't take this code literally, I'm just trying to draft some example code here and may not match perfectly your naming/data :)
Finally solved the problem.
Everything was working fine because i was launching the app from Eclipse IDE, and when packaging a .jar file, the jps files included inside the /webapp/WEB-INF/jsps folder were not included inside the .jar file and even including them throw the <resources> tag in the pom.xml file didn't solve the problem since the jar packaging isn't suitable for jsp file.
I fixed the problem by adding <packaging>war</packaging> inside the pom.xml file to change the packaging method as the .jsp files feel confortable within a .war file.
Thanks to #Marc for the help.

Resources