The rest of the post assumes that you have an installation of Cloud Foundry available to you and that you have a high level understanding of Cloud Foundry. The objective of this post is to list out of the options you have in integrating your Java application to a service instance - this demo uses mysql as a sample service to integrate with but the approach is generic enough.
Overview of the Application
The application is fairly simple Spring-Boot app, it is a REST service exposing three domain types and their relationships, representing a university - Course, Teacher and Student. The domain instances are persisted to a MySQL database. The entire source code and the approaches are available at this github location if you want to jump ahead.To try the application locally, first install a local mysql server database, on a Mac OSX box with homebrew available, the following set of commands can be run:
brew install mysql mysql.server start mysql -u root # on the mysql prompt: CREATE USER 'univadmin'@'localhost' IDENTIFIED BY 'univadmin'; CREATE DATABASE univdb; GRANT ALL ON univdb.* TO 'univadmin'@'localhost';
Bring up the Spring-Boot under cf-db-services-sample-auto:
mvn spring-boot:run
and an endpoint with a sample data will be available at http://localhost:8080/courses.
Trying this application on Cloud Foundry
If you have an installation of PCF Dev running locally, you can try out a deployment of the application the following way:cf api api.local.pcfdev.io --skip-ssl-validation cf login # login with admin/admin credentials
Create a Mysql service instance:
cf create-service p-mysql 512mb mydb
and push the app! (manifest.yml provides the binding of the app to the service instance)
cf push
An endpoint should be available at http://cf-db-services-sample-auto.local.pcfdev.io/courses
Approaches to service connectivity
Now that we have an application that works locally and on a sample local Cloud Foundry, these are the approaches to connecting to a service instance.
Approach 1 - Do nothing, let the Java buildpack handle the connectivity details
This approach is demonstrated in the cf-db-services-sample-auto project. Here the connectivity to the local database has been specified using Spring Boot and looks like this:
--- spring: jpa: show-sql: true hibernate.ddl-auto: none database: MYSQL datasource: driverClassName: com.mysql.jdbc.Driver url: jdbc:mysql://localhost/univdb?autoReconnect=true&useSSL=false username: univadmin password: univadmin
When this application is pushed to Cloud Foundry using the Java Buildpack, a component called the java-buildpack-auto-reconfiguration is injected into the application which reconfigures the connectivity to the service based on the runtime service binding.
Approach 2 - Disable Auto reconfiguration and use runtime properties
This approach is demonstrated in the cf-db-services-sample-props project. When a service is bound to an application, there is a set of environment properties injected into the application under the key "VCAP_SERVICES". For this specific service the entry looks something along these lines:"VCAP_SERVICES": { "p-mysql": [ { "credentials": { "hostname": "mysql.local.pcfdev.io", "jdbcUrl": "jdbc:mysql://mysql.local.pcfdev.io:3306/cf_456d9e1e_e31e_43bc_8e94_f8793dffdad5?user=**\u0026password=***", "name": "cf_456d9e1e_e31e_43bc_8e94_f8793dffdad5", "password": "***", "port": 3306, "uri": "mysql://***:***@mysql.local.pcfdev.io:3306/cf_456d9e1e_e31e_43bc_8e94_f8793dffdad5?reconnect=true", "username": "***" }, "label": "p-mysql", "name": "mydb", "plan": "512mb", "provider": null, "syslog_drain_url": null, "tags": [ "mysql" ] } ] }
The raw json is a little unwieldy to consume, however Spring Boot automatically converts this data into a flat set of properties that looks like this:
"vcap.services.mydb.plan": "512mb", "vcap.services.mydb.credentials.username": "******", "vcap.services.mydb.credentials.port": "******", "vcap.services.mydb.credentials.jdbcUrl": "******", "vcap.services.mydb.credentials.hostname": "******", "vcap.services.mydb.tags[0]": "mysql", "vcap.services.mydb.credentials.uri": "******", "vcap.services.mydb.tags": "mysql", "vcap.services.mydb.credentials.name": "******", "vcap.services.mydb.label": "p-mysql", "vcap.services.mydb.syslog_drain_url": "", "vcap.services.mydb.provider": "", "vcap.services.mydb.credentials.password": "******", "vcap.services.mydb.name": "mydb",
Given this, the connectivity to the database can be specified in a Spring Boot application the following way - in a application.yml file:
spring: datasource: url: ${vcap.services.mydb.credentials.jdbcUrl} username: ${vcap.services.mydb.credentials.username} password: ${vcap.services.mydb.credentials.password}
One small catch though is that since I am now explicitly taking control of specifying the service connectivity, the runtime java-buildpack-auto-reconfiguration has to be disabled, which can done by a manifest metadata:
--- applications: - name: cf-db-services-sample-props path: target/cf-db-services-sample-props-1.0.0.RELEASE.jar memory: 512M env: JAVA_OPTS: -Djava.security.egd=file:/dev/./urandom SPRING_PROFILES_ACTIVE: cloud services: - mydb buildpack: https://github.com/cloudfoundry/java-buildpack.git env: JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}'
Approach 3 - Using Spring Cloud Connectors
The third approach is to use the excellent Spring Cloud Connectors project and a configuration which specifies a service connectivity looks like this and is demonstrated in the cf-db-services-sample-connector sub-project:@Configuration @Profile("cloud") public class CloudFoundryDatabaseConfig { @Bean public Cloud cloud() { return new CloudFactory().getCloud(); } @Bean public DataSource dataSource() { DataSource dataSource = cloud().getServiceConnector("mydb", DataSource.class, null); return dataSource; } }
Pros and Cons
These are the Pros and Cons with each of these approaches:
Approaches | Pros | Cons |
---|---|---|
Approach 1 - Let Buildpack handle it | 1. Simple, the application that works locally will work without any changes on the cloud | 1. Magical - the auto-reconfiguration may appear magical to someone who does not understand the underlying flow 2. The number of service types supported is fairly limited - say for eg, if a connectivity is required to Cassandra then Auto-reconfiguration will not work |
Approach 2 - Explicit Properties | 1. Fairly straightforward. 2. Follows the Spring Boot approach and uses some of the best practices of Boot based applications - for eg, there is a certain order in which datasource connection pools are created, all those best practices just flow in using this approach. | 1. The Auto-reconfiguration will have to be explicitly disabled 2. Need to know what the flattened properties look like 3. A "cloud" profile may have to be manually injected through environment properties to differentiate local development and cloud deployment 4. Difficult to encapsulate reusability of connectivity to newer service types - say Cassandra or DynamoDB. |
Approach 3 - Spring Cloud Connectors | 1. Simple to integrate 2. Easy to add in re-usable integration to newer service types | 1. Bypasses the optimizations of Spring Boot connection pool logic. |
Conclusion
My personal preference is to go with Approach 2 as it most closely matches the Spring Boot defaults, not withstanding the cons of the approach. If more complicated connectivity to a service is required I will likely go with approach 3. Your mileage may vary though
References
1. Scott Frederick's spring-music has been a constant guide.2. I have generously borrowed from Ben Hale's pong_matcher_spring sample.
Excellent information. Disabling ' java-buildpack-auto-reconfiguration' as explained in approach 2 above is what fixed my problem. Thanks!
ReplyDeleteWonderful article, very infromative.
ReplyDeleteThank you for this excellent article! It was my main guide figuring out how to set up and deploy a Spring boot app using a database on CF.
ReplyDeletebtw - there was one thing we didn't figure out yet: using a Postgres service on Pivotal, we get environment variables like this
Delete"VCAP_SERVICES": {
"elephantsql": [
{
"credentials": {
"uri": "postgres://user:pass@server:5432/dbname
(ie. no jdbcUrl / userName / password). Therefore, we tried setting application-cloud.yml (as in your example) like this:
spring:
datasource:
uri: ${vcap.services.{postgres-servicename}.credentials.uri}
but that made it fall back to local application.yml's spring - datasource postgres connection data, which obviously failed there. Commenting out the local settings made it fail to find any data source, so there's obviously a mismatch there.
Do you know if there a way to have Pivotal come with the jdbcUrl / userName / password connection VCAP properties, or does using a uri connection string require another syntax maybe?
I couldn't find a solution to fix this today, although it was easy enough to get the application working with JBP_CONFIG_SPRING_AUTO_RECONFIGURATION set to *true*. We'd still prefer to use the manual settings.
Thanks!
Yes, good catch Lúthien, I am not entirely sure why the connection url's are munged up this way by some of the services :-). I think it may be better to depend on Spring Cloud Connectors for such scenarios(Approach 3 or Approach 1 like you found), it understands how to get the details from such uri's and put it together the right way.
DeleteThank you! We will look into that.
Delete