A Kubernetes operator for Spring Boot microservices. If you have a container image springguides/demo
with a Spring Boot application running on port 8080, you can deploy it to Kubernetes with just a few lines of YAML:
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: demo
spec:
image: springguides/demo
You can try it out in an interactive tutorial in Katakoda.
Installation
The controller is in Dockerhub, so you should be able to deploy it from just the YAML:
$ kubectl apply -f <(kustomize build github.com/dsyer/spring-boot-operator/config/default)
One Service
for the controller is installed into the spring-system
namespace:
$ kubectl get all -n spring-system
NAME READY STATUS RESTARTS AGE
pod/spring-controller-manager-79c6c95677-8hf89 2/2 Running 0 3m17s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/spring-controller-manager-metrics-service ClusterIP 10.111.94.226 <none> 8443/TCP 3m17s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/spring-controller-manager 1/1 1 1 3m17s
NAME DESIRED CURRENT READY AGE
replicaset.apps/spring-controller-manager-79c6c95677 1 1 1 3m17s
Petclinic:
$ kubectl create namespace services
$ kubectl apply -f <(kustomize build github.com/dsyer/spring-boot-operator/config/samples/mysql)
$ kubectl apply -f <(curl https://raw.githubusercontent.com/dsyer/spring-boot-operator/master/config/samples/petclinic.yaml)
Clean up:
$ kubectl delete microservices --all
$ kubectl delete namespace spring-system
Building from Source
If you know how to set up a GO lang development environment, and are building from source you can just do this:
$ make install
$ make run
and then the controller will register with your default cluster.
Tip
|
You may encounter issues with go modules. If you see this:
try this:
|
The source code has a VSCode .devcontainer
definition, so if you use the "Remote Container" extension, you should be able to run in a container. The devcontainer.json
has comments explaining what to set up on the host (you need to have docker
running and set up your ~/.kube/config
to talk to your cluster).
Try it Out
Send the sample YAML above to Kubernetes with kubectl
or Kapp:
$ kubectl apply -f <(curl https://raw.githubusercontent.com/dsyer/spring-boot-operator/master/config/samples/demo.yaml)
The Microservice
generates a Service
and a Deployment
, similar to what you would get if you used kubectl create
to generate them from your image. Example
$ kubectl apply -f config/samples/demo.yaml
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/mysql-744f7b658d-zt2gx 1/1 Running 0 26h
pod/demo-6b78fb7b85-2snj4 1/1 Running 0 62m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d2h
service/demo ClusterIP 10.104.167.30 <none> 80/TCP 62m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/demo 1/1 1 1 62m
NAME DESIRED CURRENT READY AGE
replicaset.apps/demo-6b78fb7b85 1 1 1 62m
The Service
is listening on port 80, so you can expose it locally using a port forward:
$ kubectl port-forward svc/demo 8080:80
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
and then in another terminal
$ curl localhost:8080/actuator | jq .
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/actuator/info",
"templated": false
}
}
}
There are more features, expressing opinions about how pods should be defined when Spring Boot applications are being deployed.
Spring Profiles
Spring Profiles can be activated by putting profiles
in the Microservice
spec (as an array). Example:
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: demo
spec:
image: springguides/demo
profiles:
- mysql
The effect is to generate an EnvVar
in the Deployment
with SPRING_PROFILES_ACTIVE=mysql
.
Bindings
If your namespace has backend services, like databases, which can be exposed as CNB Bindings, then you can list them in the Microservice
spec. There is a CRD for ServiceBinding
which developers (or operators) can use to define the behaviour of the of all Microservice
instances in the same namespace. Example:
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: bindings
spec:
image: springguides/demo
bindings:
- mysql
profiles:
- mysql
Each binding is in the form [namespace/]<name>
where the name space is optional. It is used to search for a ServiceBinding
in the namespace specified (or the same namepsace as the Microservice
if not specified, as in this example).
Actuators
If your application container has Spring Boot Actuators then it probably makes sense to use them as Kubernetes probes. You can do that in one line (accepting the default configuration of liveness and readiness probes):
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: actr
spec:
image: springguides/demo
bindings: actuators
The default binding for "actuators" is a liveness probe on /actuator/info
and a readiness probe on /actuator/health
. You can change the probe configurations if you need to using a custom binding.
Custom Bindings
A binding carries a patch for the PodTemplateSpec
in the app Deployment
. It can add a restart policy, annotations, volumes, containers, and init containers, or it can modify the "app" container. Containers can be patched using the volume mounts, env vars, image, command, args, or working dir properties. For example:
apiVersion: spring.io/v1
kind: ServiceBinding
metadata:
name: prometheus
spec:
template:
metadata:
annotations:
prometheus.io/path: /actuator/prometheus
prometheus.io/port: "8080"
prometheus.io/scrape: "true"
This one adds the annotations that are needed by the Prometheus Helm chart installation to pull metrics from the Spring Boot Actuator endpoint.
An additional feature is that a ServiceBinding
can separately specify environment variables for the main app container, in a form that helps with some of the naming conventions in Spring Boot. In particular it permits environment variables which bind to a string array in Spring Boot to accumulate additional content in multiple ServiceBindings
. For example, these two bindings applied to a single Microservice
will expose the metrics
and env
Actuator endpoints, in addition to the default info
and health
:
apiVersion: spring.io/v1
kind: ServiceBinding
metadata:
name: metrics
spec:
env:
- name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE
values:
- info
- health
- metrics
---
apiVersion: spring.io/v1
kind: ServiceBinding
metadata:
name: env
spec:
env:
- name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE
values:
- info
- health
- env
EnvVar
entries in a ServiceBinding
can have a single value
or multiple values
. In the case of a single value
the last one to bind wins. With multiple values
they are merged and written into the app container as a comma-separated list.
CNB Bindings
Services are bound to by name (optionally prefixed with <namespace>/
). A useful pattern is to implement the CNB Bindings spec, namely that a binding named <binding>
creates directories in the Pod
via VolumeMounts
at ${CNB_BINDINGS}/<binding>/metadata
and ${CNB_BINDINGS}/<binding>/secret
. A good way to do that is to create a ConfigMap
called <binding>-metadata
and optionally a Secret
called <binding>-secret
. The ConfigMap
should have at least the kind
, provider
and tags
entries since those are mandatory for CNB Bindings.
There is an init container that you can use to convert CNB bindings to Spring Boot configuration files. It copies the configuration entries from the binding config maps and secrets into /etc/config/application.properties
. The SPRING_CONFIG_LOCATION
can then also be set to pick up this location so your application will see those properties as higher priority than those on the classpath, but still lower than system properties or environment variables.
For example if there is a ConfigMap
and a Secret
, the application.properties
entries for the MySQL example might come out like this:
cnb.metadata.other.host=mysql
cnb.metadata.other.kind=mysql
cnb.metadata.other.provider=dsyer
cnb.metadata.other.tags=database,sql
cnb.secret.other.password=test
cnb.secret.other.user=test
cnb.secret.other.database=test
The kind
of the Binding.Metadata
is also used as a key to locate a transformation rule. The rule is expressed as a set of GO templates that can be rendered from the binding. The templates can be customized by developers (or operators) by including them in the config map (or as a separate config map) and mounting them at ${CNB_BINDINGS}/../templates/<binding>
.
There is a sample MySQL service in the project which exposes the right config maps and secrets: look in config/samples/mysql
. The MySQL example generates these properties in addition to the cnb.*
ones:
spring.datasource.url=jdbc:mysql://mysql/test
spring.datasource.username=test
spring.datasource.password=test
A Spring Boot application with mysql-connector
will automatically connect because it matches the default naming conventions in spring-boot-autoconfigure
.
Default (Non-Actuators) Bindings
Services are bound to by name (optionally prefixed with <namespace>/
). If there is no binding at the namespaced location specified, then a default one is created. Any other binding than "actuators" generates a CNB style ServiceBinding
, namely it assumes the existence of a ConfigMap
called <binding>-metadata
and a Secret
called <binding>-secret
. The ConfigMap
should have at least the kind
, provider
and tags
entries since those are mandatory for CNB Bindings.
Pod Specs
The PodTemplateSpec
in the Deployment
can be supplied directly in the Microservice
spec if desired. The Spring Boot application runs in a Container
called "app" by convention (or the first container if there is none called "app"), so any configuration of that Pod
in the Microservice
is applied to the Deployment
. For example, to set an environment variable:
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: env
spec:
image: springguides/demo
template:
spec:
containers:
# the "app" container is special - it doesn't need an image
- name: app
env:
- name: EXT_LIBS
value: /app/ext
You could add your own probes here, volume mounts, whatever you need to customize the application container. The image is always set to the one in the top of the MicroService
spec.
Jobs
Instead of a Deployment
and a Service
, a MicroService
can be a short-lived process, implemented as a Job
in Kubernetes. Just make sure the app
container is short-lived, and set the job
flag in the MicroService
. Example:
apiVersion: spring.io/v1
kind: Microservice
metadata:
name: job
spec:
job: true
image: busybox
args:
- /bin/sh
- -c
- env; find /var/run
Because of the way Kubernetes works, you cannot mutate a Job
(e.g. change its Pod
spec) once it has started. You need a different name, or to delete the old MicroService
, or the old Job
instance, in order to run another one.