In this blogpost we are showing how to deploy a Spring Boot application to OpenShift levaraging the fabric8-maven-plugin and Spring Cloud Kubernetes
Minishift setup
First we need an OpenShift cluster. On Mac is very easy to get started:
$ brew cask install minishift
$ brew cask install virtualbox
By default, Minishift will use the relevant hypervisor based on the host operating system,
here we instruct to use VirtualBox
instead of xhyve
on OSX.
$ minishift start --vm-driver=virtualbox
We can verify that our single node OpenShift cluster is running using:
$ minishift status
Minishift: Running
Profile: minishift
OpenShift: Running (openshift v3.11.0+3a34b96-217)
DiskUsage: 57% of 19G (Mounted On: /mnt/sda1)
CacheUsage: 1.677 GB (used by oc binary, ISO or cached images)
and get the cluster IP using:
$ minishift ip
192.168.99.101
At the time of writing we were using the Minishift version:
$ minishift version
minishift v1.33.0+ba29431
OpenShift Client
Next, we need to install the OpenShift Client
in order to interact with our OpenShift cluster from the command line:
$ brew install openshift-cli
$ oc version
Client Version: version.Info{Major:"4", Minor:"1+", GitVersion:"v4.1.0+b4261e0", GitCommit:"b4261e07ed", GitTreeState:"clean", BuildDate:"2019-05-18T05:40:34Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11+", GitVersion:"v1.11.0+d4cacc0", GitCommit:"d4cacc0", GitTreeState:"clean", BuildDate:"2019-07-05T15:07:29Z", GoVersion:"go1.10.8", Compiler:"gc", Platform:"linux/amd64"}
Next we login with developer/developer
$ oc login
Authentication required for https://192.168.99.101:8443 (openshift)
Username: developer
Password:
OpenShift project
We create a new project called boot
$ oc new-project boot
Now using project "boot" on server "https://192.168.99.101:8443".
$ oc status
In project boot on server https://192.168.99.101:8443
You have no services, deployment configs, or build configs.
Run 'oc new-app' to create an application.
PostgreSQL service
For the example we will need a PostgreSQL
service. In order to provision this service we are using the OpenShift Web Console
provided at
https://<minishift-ip>:8443/console/
After logging in as developer
/developer
we select the project boot
created in the previous step.
We make sure that the boot
project is be visible in the top left corner, otherwise when creating resources we might create them in another project.
On the left we select the catalog
, then Databases
and then Postgres
.
Then we select PostgreSQL
we click on Next
, then on Create
, leaving everything at default. This will create a PostgreSQL
database.
The following service(s) have been created in your project: boot.
Username: userT0G
Password: MMkglTP6tRe5mJL0
Database Name: sampledb
Connection URL: postgresql://postgresql:5432/
If we look around in the OpenShift Web Console
we will see that many things were created: a deployment
, with one pod running, a persistent volume claim
, a secret
.
In the terminal we can also view the created service using:
$ oc get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgresql ClusterIP 172.30.1.92 <none> 5432/TCP 4m
Note, that with the following command we can always verify in which project we are:
oc project
Using project "boot" on server "https://192.168.99.101:8443".
Lets connect to recently created postgresql
service and create a customers
table
$ oc get pods
NAME READY STATUS RESTARTS AGE
postgresql-1-zzrlp 1/1 Running 0 30m
$ oc rsh postgresql-1-zzrlp
sh-4.2$ psql sampledb userT0G
psql (9.6.10)
Type "help" for help.
sampledb=
sampledb=# create table customers (id BIGSERIAL, first_name varchar(255) not null, last_name varchar(255) not null, PRIMARY KEY (id));
CREATE TABLE
sampledb=# insert into customers values (1, 'Sam', 'Sung');
INSERT 0 1
sampledb=# \d
List of relations
Schema | Name | Type | Owner
--------+------------------+----------+----------
public | customers | table | postgres
public | customers_id_seq | sequence | postgres
(2 rows)
In order to disconnect from the postgresql
and container
use:
sampledb-# \q
sh-4.2$ exit
exit
command terminated with exit code 127
Demo application
We get the bits of the demo application found at https://github.com/altfatterz/openshift-spring-boot-demo
$ git clone https://github.com/altfatterz/openshift-spring-boot-demo
The demo contains two Spring Boot applications customer-service
and order-service
.
Let’s build the applications from the openshift-spring-boot-demo
folder
$ mvn clean install
In the logs we see that is using the fabric8-maven-plugin
to generate the openshift resource descriptors (via fabric8:resource
goal) and docker images (via fabric8:build
) for us.
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>fabric8-maven-plugin</artifactId>
<version>${fabric8.maven.plugin.version}</version>
<executions>
<execution>
<id>fmp</id>
<goals>
<goal>resource</goal>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
The fabric8:build
goal is using by default the Openshift
mode with S2I
(source-to-image) strategy.
[INFO] F8: Running in OpenShift mode
[INFO] F8: Using OpenShift build with strategy S2I
The generated image is based on CentOS and supports Java 8 and 11. https://hub.docker.com/r/fabric8/s2i-java
The Source-to-Image
means that a build is done by the OpensShift cluster. We can check the created pods which were created while running the builds.
$ oc get pods
customer-service-s2i-1-build 0/1 Completed 0 2m
order-service-s2i-1-build 0/1 Completed 0 1m
...
The created docker images are stored in OpenShift’s integrated Docker Registry:
$ oc get imagestream
NAME DOCKER REPO TAGS UPDATED
customer-service 172.30.1.1:5000/boot/customer-service latest 14 minutes ago
order-service 172.30.1.1:5000/boot/order-service latest 14 minutes ago
More information about Builds and Image Streams
can be found here
We can start the customer-service
application locally using:
$ java -jar customer-service/target/customer-service-0.0.1-SNAPSHOT.jar --spring.profiles.active=local
With the local
Spring profile is using an embedded H2 database. Let’s access the /customers
endpoint:
$ http :8081/customers
HTTP/1.1 401
The endpoint requires a JWT token, where the JWT token secret can be externally configured.
After creating a JWT token (see JwtTokenGenerator
) and sending it as an Authorization
header with get a 200 response:
$ export HEADER='Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huZG9lIiwiYXV0aG9yaXRpZXMiOiJ1c2VyIn0.wUZ3aXYwmn4RoW1Tvpwq6x_AsAm4PdcD6U417SgzFfg'
$ http :8081/customers $HEADER
HTTP/1.1 200
[
{
"firstName": "John",
"id": 1,
"lastName": "Doe"
},
{
"firstName": "Jane",
"id": 2,
"lastName": "Doe"
}
]
ConfigMap and Secret
Before deploying the customer-service
application we create a ConfigMap
and Secret
with keys that the customer-service
is using:
oc create configmap openshift-spring-boot-demo-config-map \
--from-literal=greeting.message="What is OpenShift?"
oc create secret generic openshift-spring-boot-demo-secret \
--from-literal=greeting.secret="H3ll0" \
--from-literal=jwt.secret="demo" \
--from-literal=spring.datasource.username="userT0G" \
--from-literal=spring.datasource.password="MMkglTP6tRe5mJL0"
To view the created resources we can use:
oc get configmap openshift-spring-boot-demo-config-map -o yaml
oc get secret openshift-spring-boot-demo-secret -o yaml
Deploy to OpenShift
To deploy we use the following command in the customer-service
folder
$ mvn fabric8:deploy
A route
will be created for the customer-service
$ oc get route
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
customer-service customer-service-boot.192.168.99.101.nip.io customer-service 8080 None
We can see that the JWT token secret is found from the OpenShift Secret and that the application connects successfully to the postgresql
service.
$ http customer-service-boot.192.168.99.101.nip.io/customers $HEADER
HTTP/1.1 200
[
{
"id": 1,
"firstName": "Sam",
"lastName": "Sung"
}
]
For more information about the deployment we can use:
$ oc describe dc customer-service
Name: customer-service
Namespace: boot
Created: 23 minutes ago
Labels: app=customer-service
group=com.example
provider=fabric8
version=0.0.1-SNAPSHOT
Annotations: fabric8.io/git-branch=master
fabric8.io/git-commit=98a8f456aab264ffd7cdf6b6e59779522f694828
fabric8.io/scm-tag=HEAD
fabric8.io/scm-url=https://github.com/spring-projects/spring-boot/spring-boot-starter-parent/customer-service
Latest Version: 1
Selector: app=customer-service,group=com.example,provider=fabric8
Replicas: 1
Triggers: Config, Image(customer-service@latest, auto=true)
Strategy: Rolling
Template:
Pod Template:
Labels: app=customer-service
group=com.example
provider=fabric8
version=0.0.1-SNAPSHOT
Annotations: fabric8.io/git-branch: master
fabric8.io/git-commit: 98a8f456aab264ffd7cdf6b6e59779522f694828
fabric8.io/scm-tag: HEAD
fabric8.io/scm-url: https://github.com/spring-projects/spring-boot/spring-boot-starter-parent/customer-service
Containers:
spring-boot:
Image: 172.30.1.1:5000/boot/customer-service@sha256:872969674a0af4e8973ae5206b8c4b854247d1319bc5638371fb5b09f7261b51
Ports: 8080/TCP, 9779/TCP, 8778/TCP
Host Ports: 0/TCP, 0/TCP, 0/TCP
Liveness: http-get http://:8080/actuator/health delay=180s timeout=1s period=10s #success=1 #failure=3
Readiness: http-get http://:8080/actuator/health delay=10s timeout=1s period=10s #success=1 #failure=3
Environment:
GREETING_MESSAGE: <set to the key 'greeting.message' of config map 'openshift-spring-boot-demo-config-map'> Optional: false
GREETING_SECRET: <set to the key 'greeting.secret' in secret 'openshift-spring-boot-demo-secret'> Optional: false
JWT_SECRET: <set to the key 'jwt.secret' in secret 'openshift-spring-boot-demo-secret'> Optional: false
SPRING_DATASOURCE_USERNAME: <set to the key 'spring.datasource.username' in secret 'openshift-spring-boot-demo-secret'> Optional: false
SPRING_DATASOURCE_PASSWORD: <set to the key 'spring.datasource.password' in secret 'openshift-spring-boot-demo-secret'> Optional: false
KUBERNETES_NAMESPACE: (v1:metadata.namespace)
Mounts: <none>
Volumes: <none>
Deployment #1 (latest):
Name: customer-service-1
Created: 23 minutes ago
Status: Complete
Replicas: 1 current / 1 desired
Selector: app=customer-service,deployment=customer-service-1,deploymentconfig=customer-service,group=com.example,provider=fabric8
Labels: app=customer-service,group=com.example,openshift.io/deployment-config.name=customer-service,provider=fabric8,version=0.0.1-SNAPSHOT
Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal DeploymentCreated 23m deploymentconfig-controller Created new replication controller "customer-service-1" for version 1
The above worked because we provided a YAML fragment (deployment.yml
) in the /src/main/fabric8
folder which the Fabric8 Maven plugin
used when creating the deployment configuration.
The plugin adds also liveness
and readiness
probes for the correct /actuator/health
endpoint.
spec:
template:
spec:
containers:
- env:
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: openshift-spring-boot-demo-secret
key: spring.datasource.username
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: openshift-spring-boot-demo-secret
key: spring.datasource.password
We are leveraging the Spring Cloud Kubernetes
project which defaults to kubernetes
profile when running on OpenShift
cluster, so in our application-kubernetes.yml
we configured:
spring:
datasource:
url: jdbc:postgresql://postgresql:5432/sampledb
The username
and password
for the postgresql
service is resolved from the openshift-spring-boot-demo-secret
secret.
ConfigMap reload
After deploying the order-service
application as well with mvn fabric8:deploy
we will see two routes:
$ oc get routes
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
customer-service customer-service-boot.192.168.99.101.nip.io customer-service 8080 None
order-service order-service-boot.192.168.99.101.nip.io order-service 8080 None
Both services resolve the greeting.message
from the openshift-spring-boot-demo-config-map
ConfigMap as configued in the /src/main/fabric8/deloyment.yml
YAML fragment
$ http customer-service-boot.192.168.99.101.nip.io/message
What is OpenShift?
$ http order-service-boot.192.168.99.101.nip.io/message
What is OpenShift?
Spring Cloud Kubernetes
enables us that when a ConfigMap value is changed then application is reloaded. There are different levels of reload: refresh
, restart_context
and shutdown
. In this example we are using restart_context
where the Spring ApplicationContext is gracefully restarted and the beans are created with the new configuration.
spring:
cloud.kubernetes.reload:
enabled: true
strategy: restart_context
management:
endpoint:
restart:
enabled: true
In order this two work we still need to configure the common openshift-spring-boot-demo-config-map
ConfigMap as a value of the spring.cloud.kubernetes.config.name
key in the bootstrap.yml
.
The easiest way to change the Config Map is with OpenShift Web Console
under Resources
> Config Maps
. Select openshift-spring-boot-demo-config-map
then Actions > Edit
. After changing the greeting.message
message to What is Kubernetes?
both of our applications are restarted and then they serve the new value:
$ http customer-service-boot.192.168.99.101.nip.io/message
What is Kubernetes?
$ http order-service-boot.192.168.99.101.nip.io/message
What is Kubernetes?
Conclusion
In this blogpost, we
- installed Minishift and OpenShift Console
- provisioned a PostgreSQL service
- deployed a Spring Boot application to Minishift which used a provisioned PostgreSQL service
- configured the JWT token secret as an OpenShift Secret
- shared a ConfigMap between two different Spring Boot services with reload enabled
You can find the source code here: https://github.com/altfatterz/openshift-spring-boot-demo