Spring Cloud Config is a great tool to externalize configuration in a microservice architecture. It has server and client side support. In this blog post we are going to look into how to set up the server part locally and on Cloud Foundry and use it with a client application.
Config Server running locally
The config server can be embedded in a Spring Boot application using the @EnableConfigServer
annotation, but here we look into how to run it using the Spring Boot CLI with Spring Cloud CLI extension installed.
The spring cloud --list
lists the available services which can be started
spring cloud --list
configserver dataflow eureka h2 hystrixdashboard kafka zipkin
By default running spring cloud configserver
it will run in the “native” profile and serving configuration from the local directory ./launcher.
We can customise the config server with a configserver.yml
file inside the ~/.spring-cloud
folder
spring:
profiles:
active: git
cloud:
config:
server:
git:
uri: file://${user.home}/projects/cloudfoundry/config-repo
Inside the config-repo
we create an example foo-service.yml
(foo-service will be our client application) file with the content
foo:
message: Hallo
secret: '{cipher}6050a8af7826c89dcd30c9046f484a5a169d27beb40a67db72440474c5cc6d77'
The foo.secret
value was encrypted using
spring encrypt Gruetzi --key s3cr3t
We set the ENCRYPT_KEY
environment variable before starting the config server
export ENCRYPT_KEY=s3cr3t
And we start up the config server
using
spring cloud configserver
We can easily verify that the config server
knows about the external configuration of the foo-service
http :8888/foo-service/default
{
"label": null,
"name": "foo-service",
"profiles": [
"default"
],
"propertySources": [
{
"name": "file:///Users/zoal/projects/cloudfoundry/config-repo/foo-service.yml",
"source": {
"foo.message": "Hallo",
"foo.secret": "6050a8af7826c89dcd30c9046f484a5a169d27beb40a67db72440474c5cc6d77"
}
}
],
"state": null,
"version": null
}
After starting the foo-service
client application we can see in the logs:
[ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:8888
The foo-service
is using config-first bootstrap
and with the spring.cloud.config.uri
locates the config server which is set to http://localhost:8888
by default.
With a simple controller we print out the foo.message
and foo.secret
properties which we can verify with
http :8080 -a user:password
Hallo/Gruetzi
Config Server on Pivotal Cloud Foundry (PCF)
So far so good. Let’s setup this little example on a Pivotal Web Services
(an instance of PCF hosted by Pivotal) where we will use the Config Server from Spring Cloud Service.
First we need to install the CloudFoundry CLI and create a Pivotal Web Services account.
After we pushed the config-rep to GitHub we can configure a config-server
service instance with this shared git repository.
We will use the p-config-server
service with the trial
plan, which we found from that is available when as a response to cf marketplace
We create a service instance named config-server
with the following command
cf cs p-config-server trial config-server -c config-server-setup.json
where the config-server-setup.json
contains the shared git repository:
{
"git": {
"uri": "https://github.com/altfatterz/config-repo.git"
}
}
We wait until the service instance is created, the status can be easily checked using cf service config-server
.
Next we need to set the encrypt key for the config-server
. By default the config-server
will decrypt the secrets and return the decrypted secrets to the client applications like in our case the to the foo-service
. It is important that we protect the config server
and have https connection between the config server
and its client applications.
It is also possible that the client applications decrypt the secrets locally. In this case we need to set the spring.cloud.config.server.encrypt.enabled=false
in the bootstrap.yml
and set the encrypt key for the client applications.
In Pivotal Web Services
from the Config Server Dashboard
we can get the config-server
service instance configuration and by adding the "encrypt":{"key":"s3cr3t"}
we can update the service instance:
cf update-service config-server -c '{"count":1,"git":{"uri":"https://github.com/altfatterz/config-repo.git"}, "encrypt":{"key":"s3cr3t"} }'
Then we have to add the following dependency to the foo-service
:
<dependency>
<groupId>io.pivotal.spring.cloud</groupId>
<artifactId>spring-cloud-services-starter-config-client</artifactId>
</dependency>
edit the bootstrap.yml
configuration file
spring:
cloud:
config:
uri: ${vcap.services.config-server.credentials.uri:http://localhost:8888}
and add the mainfest.yml
file
applications:
- name: foo-service
host: foo-service
memory: 756M
instances: 1
path: target/foo-service-0.0.1-SNAPSHOT.jar
services:
- config-server
and deploy the service using cf push
from the foo-service
directory.
After we verify that the app is up and running via cf service foo-service
we can test our service using
http foo-service.cfapps.io -a user:password
Hallo/Gruetzi
Let’s check how does this work. When the foo-service
was bound the to config-server
service instance the connection details were set in the VCAP_SERVICES
environment variable.
cf env foo-service
{
"VCAP_SERVICES": {
"p-config-server": [
{
"binding_name": null,
"credentials": {
"access_token_uri": "https://p-spring-cloud-services.uaa.run.pivotal.io/oauth/token",
"client_id": "p-config-server-9c1a682a-089d-49be-97e0-1ad1f83366a1",
"client_secret": "a2MRVZLjUyCy",
"uri": "https://config-7e4b48ae-efd6-4773-b377-c8d1edf5ced6.cfapps.io"
},
"instance_name": "config-server",
"label": "p-config-server",
"name": "config-server",
"plan": "trial",
"provider": null,
"syslog_drain_url": null,
"tags": [
"configuration",
"spring-cloud"
],
"volume_mounts": []
}
]
}
}
We can see that the communication between the config-server
service instance and its client applications is via OAuth 2.0 protocol. The client must obtain first an OAuth 2.0 token in order to make requests directly against the config-server
.
The included spring-cloud-services-starter-config-client
dependency handles these OAuth 2.0 requests behind the scene for us.
curl https://p-config-server-9c1a682a-089d-49be-97e0-1ad1f83366a1:[email protected]/oauth/token -d grant_type=client_credentials
We get an access token like this. Note that according to the response the token is only valid for 29 seconds.
{
"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIzN2MxZjczNTMwMDc0OTQ2ODFjZTIxNzFiZWIxZDllZiIsInN1YiI6InAtY29uZmlnLXNlcnZlci05YzFhNjgyYS0wODlkLTQ5YmUtOTdlMC0xYWQxZjgzMzY2YTEiLCJhdXRob3JpdGllcyI6WyJwLWNvbmZpZy1zZXJ2ZXIuN2U0YjQ4YWUtZWZkNi00NzczLWIzNzctYzhkMWVkZjVjZWQ2LnJlYWQiXSwic2NvcGUiOlsicC1jb25maWctc2VydmVyLjdlNGI0OGFlLWVmZDYtNDc3My1iMzc3LWM4ZDFlZGY1Y2VkNi5yZWFkIl0sImNsaWVudF9pZCI6InAtY29uZmlnLXNlcnZlci05YzFhNjgyYS0wODlkLTQ5YmUtOTdlMC0xYWQxZjgzMzY2YTEiLCJjaWQiOiJwLWNvbmZpZy1zZXJ2ZXItOWMxYTY4MmEtMDg5ZC00OWJlLTk3ZTAtMWFkMWY4MzM2NmExIiwiYXpwIjoicC1jb25maWctc2VydmVyLTljMWE2ODJhLTA4OWQtNDliZS05N2UwLTFhZDFmODMzNjZhMSIsImdyYW50X3R5cGUiOiJjbGllbnRfY3JlZGVudGlhbHMiLCJyZXZfc2lnIjoiOWZlZjNmZDYiLCJpYXQiOjE1MzIxODkwMTQsImV4cCI6MTUzMjE4OTA0NCwiaXNzIjoiaHR0cHM6Ly9wLXNwcmluZy1jbG91ZC1zZXJ2aWNlcy51YWEucnVuLnBpdm90YWwuaW8vb2F1dGgvdG9rZW4iLCJ6aWQiOiIwNDlmYjNmOC1mOTc2LTQ1YTQtOWVhMy0wMDRkNDU2ZDRlMzgiLCJhdWQiOlsicC1jb25maWctc2VydmVyLTljMWE2ODJhLTA4OWQtNDliZS05N2UwLTFhZDFmODMzNjZhMSIsInAtY29uZmlnLXNlcnZlci43ZTRiNDhhZS1lZmQ2LTQ3NzMtYjM3Ny1jOGQxZWRmNWNlZDYiXX0.NDKKceOUv_WigMox38xoE7VuDRi6uz3zddFuu7F8Wz7UtLWgZ3wgLyzeKfT2f4gljgEOqT5XU3CJRazVHpbCdgTkhb4M37_sgFnHOZPYFgjN3GneSfT3ZLNhKyf36OTxbPA6sIgBYfFc1i6NDyG5y8CeLdDxEBkm5GMDtlvP-rptDhokeSlnJ6J1M6pJQZd7wYDqxZPfpvLesPE--yjUmRDO4Np8dF5DSOhSGM1El5HmCFJhy6lze1fizaLsGtbnqPIbZLlceENoullvHT-dt0w_Wxf9ErcUp_DIi3BMo5fUSCVyNLfTG965aye16_9v2IL9A08CnThxcCWZvk6Z1A",
"token_type":"bearer",
"expires_in":29,
"scope":"p-config-server.7e4b48ae-efd6-4773-b377-c8d1edf5ced6.read",
"jti":"37c1f7353007494681ce2171beb1d9ef"
}
And with this token the client application can send direct requests to the config-server
:
http https://config-7e4b48ae-efd6-4773-b377-c8d1edf5ced6.cfapps.io/foo-service/default 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyMTE3NGUwMjNiNzU0ZTg3YWYxNTczYmUzNTI3YjJmYSIsInN1YiI6InAtY29uZmlnLXNlcnZlci05YzFhNjgyYS0wODlkLTQ5YmUtOTdlMC0xYWQxZjgzMzY2YTEiLCJhdXRob3JpdGllcyI6WyJwLWNvbmZpZy1zZXJ2ZXIuN2U0YjQ4YWUtZWZkNi00NzczLWIzNzctYzhkMWVkZjVjZWQ2LnJlYWQiXSwic2NvcGUiOlsicC1jb25maWctc2VydmVyLjdlNGI0OGFlLWVmZDYtNDc3My1iMzc3LWM4ZDFlZGY1Y2VkNi5yZWFkIl0sImNsaWVudF9pZCI6InAtY29uZmlnLXNlcnZlci05YzFhNjgyYS0wODlkLTQ5YmUtOTdlMC0xYWQxZjgzMzY2YTEiLCJjaWQiOiJwLWNvbmZpZy1zZXJ2ZXItOWMxYTY4MmEtMDg5ZC00OWJlLTk3ZTAtMWFkMWY4MzM2NmExIiwiYXpwIjoicC1jb25maWctc2VydmVyLTljMWE2ODJhLTA4OWQtNDliZS05N2UwLTFhZDFmODMzNjZhMSIsImdyYW50X3R5cGUiOiJjbGllbnRfY3JlZGVudGlhbHMiLCJyZXZfc2lnIjoiOWZlZjNmZDYiLCJpYXQiOjE1MzIxODk4MTgsImV4cCI6MTUzMjE4OTg0OCwiaXNzIjoiaHR0cHM6Ly9wLXNwcmluZy1jbG91ZC1zZXJ2aWNlcy51YWEucnVuLnBpdm90YWwuaW8vb2F1dGgvdG9rZW4iLCJ6aWQiOiIwNDlmYjNmOC1mOTc2LTQ1YTQtOWVhMy0wMDRkNDU2ZDRlMzgiLCJhdWQiOlsicC1jb25maWctc2VydmVyLTljMWE2ODJhLTA4OWQtNDliZS05N2UwLTFhZDFmODMzNjZhMSIsInAtY29uZmlnLXNlcnZlci43ZTRiNDhhZS1lZmQ2LTQ3NzMtYjM3Ny1jOGQxZWRmNWNlZDYiXX0.RE0MBHDPolEeiz9Y9R0kb1_rzdyCr1c7AQA3zz4qiPJDW0DsnOFa-_GS-z7eyrz4QCanvFmd9m9WpRsh_Y4bnATPiYclqdXy65-Ziu3FKZka8Tv8WQ7dNOM520d2Dn6PcDapz8hJ7zoPJ9RxS2daZVgNkhzgFQOLxzk6t5coecV8ccN4-lbF3yK_E0aHe2QsIyZSeiQoBWsKC1r_8wZSgVbCFcQ7WkcSS9MIQ9SOxyLpIKsxjDj2JASw7MNID5iBJ8J_-0FfNtu710j7ZhHOh_4Xy9r399ie34NvdQKHTjqoctBjEKUD6CqaUwuYQWIXNj__ox44ex3RXAEq0d92Jg'
{
"label": null,
"name": "foo-service",
"profiles": [
"default"
],
"propertySources": [
{
"name": "https://github.com/altfatterz/config-repo.git/foo-service.yml",
"source": {
"foo.message": "Hallo",
"foo.secret": "Gruetzi"
}
}
],
"state": null,
"version": "3635a8a138bcac086c66e55013ae1595aded0045"
}