An important principle in continuous delivery is to build application binaries only once and let the exact same binary flow through each step of the build pipeline in order to verify that the binary is ready for production.
A typical build pipeline might look something like this:
- Commit stage (build, unit test, analysis)
- Automated acceptance tests
- Capacity testing
- User acceptance testing
- Production
Many of these steps will often require different application configuration. We might want to stub out some backend services, use different username/password for each environment, etc.
Unfortunately there’s a convention in the Java EE spec to package the configuration inside the war or ear file together with the application. This means that we have to configure the application build time. When we do this we have to create a different application binary for different deployment environments. This is a serious risk and an anti pattern in continuous delivery.
Externalising configuration with Apache ZooKeeper
Apache ZooKeeper is, among many things, a centralised service for maintaining configuration information. ZooKeeper maintains a tree-structure with nodes and child-nodes. Each node in ZooKeeper can have data as well as children associated with it. This is a great data structure for storing configuration. ZooKeeper also provides a mechanism called watches, where clients can listen for changes. This means that we can update configuration at runtime.
There’s an official Docker image for Apache Zookeeper that will help you get Zookeeper up and running.
We’ll connect to ZooKeeper using the CLI and add some nodes to store our configuration:
create /myapp root
create /myapp/config config
create /myapp/config/endpoints endpoints
create /myapp/config/endpoints/twitter my-twitter-test.com
Using Curator to connect to ZooKeeper from Java EE
ZooKeeper has a very low-level API and Apache Curator is a framework client library for ZooKeeper that has a nice API and is quite easy to work with.
The first thing we need to do is to add the curator client dependencies in our pom:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>2.9.0</version>
</dependency>
Too instantiate curator we will use a @Startup @Singleton bean. The url to ZooKeeper should be provided as an application parameter (avoid build time configuration, remember).
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import java.util.HashMap;
import java.util.Map;
@Singleton
@Startup
public class ZooKeeperRegistry {
private String zookeeperHost = "127.0.0.1:2181"; // use jvm argument
private CuratorFramework client;
private Map<String, String> properties = new HashMap<>();
public String getProperty(String key) {
return properties.get(key);
}
public Map<String, String> getProperties() {
return properties;
}
@PostConstruct
private void postConstruct() throws Exception {
// establish connection to ZooKeeper
RetryPolicy retryPolicy = new ExponentialBackoffRetry(10000, 3);
client = CuratorFrameworkFactory.newClient(zookeeperHost, retryPolicy);
client.start();
// get data for node, add a watcher to retrieve updates
byte[] endpoint = client.getData().usingWatcher(new MyWatcher()).forPath("/myapp/config/endpoints/twitter");
properties.put("twitter-endpoint", new String(endpoint));
}
@PreDestroy
private void preDestroy() {
client.close();
}
private class MyWatcher implements CuratorWatcher {
@Override
public void process(WatchedEvent event) throws Exception {
if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
// get changed data, re-register the watcher to get further notifications
byte[] bytes = client.getData().usingWatcher(this).forPath(event.getPath());
properties.put("twitter", new String(bytes));
}
}
}
}
This is just a simple example on how you can store your configuration data outside the war or ear.
There are many management and visualisation tools available for ZooKeeper and there are plugins for both Intellij and Eclipse. ZooKeeper also comes bundled with a gui tool called ZooInspector.
There are also many alternatives to ZooKeeper, like Spring Config Server, Consul and etcd.
You can also just use simple jvm-arguments or store configuration in a database, but the most important point is to remove the build-time dependency.