Self-contained Java services
Johnathan Gilday
Next Century Corporation
2016-07-14
Some motivation
- At Next Century, we build a lot of services for our customers, often in Java
- Our services are getting trimmer and more plentiful
- Our development teams continue to become more involved with the infrastructure management and production operations
We need to optimize the way we build and deploy these services. Abandoning the shared application container is one way
What do I mean by container?
- Also called "application server"
- One process
- Manages one or more archives (WAR, EAR)
- Examples: Apache Tomcat, Wildfly, Glassfish
To deploy application archives to running containers:
- Install container
- Build WAR
- Push WAR to container
Containers Manage your app's lifecycle
@WebListener
public class AppInitializer implements ServletContextListener {
@Override
public void contextInitialized(final ServletContextEvent sce) {
// container calls this method to initialize app
}
@Override
public void contextDestroyed(final ServletContextEvent sce) {
// container calls this method to shutdown app
}
}
@WebServlet(name = "my-servlet", urlPatterns = "/my-servlet")
public class MyServlet extends HttpServlet {
@Override
public void doGet(final HttpServletRequest req, final HttpServletResponse resp) {
// container calls this method on incoming request
}
}
When are containers helpful?
Development Team
-
Delivers a packaged application with dependencies outside of those provided by the container
-
Delivers a configuration files and documentation for running the service
Operations Team
- Configures the operating system
- Configures the container
- May run multiple applications on the container
Containers work best when there is a clear separation of responsibilities between teams developing and maintaining the application
What if there's just one team?
Discrepancies between application and container cause headaches
- Logging
- Configuration
- Dependencies (classpath)
- Test environments (mvn jetty:run vs container)
Environment discrepancies cause headaches
- Different versions of dependencies on the classpath
- Example: container provides a different version of servlet than the one used in testing
- Containers use varying strategies for classpath loading
Proposed Solution:
Deliver self-contained applications
Self-contained apps
- Live in their own process
- Embed their own web server
- Embed all their own dependencies
- Define their own means of configuration
What does developing a self-contained app look like?
Runs in its own process
/**
* Point of entry. Configure and start app
*/
public class App {
public static void main(final String[] args) {
// configure application
// register signal handlers if desired
// run web server until app terminates
}
}
- Embraces Unix process model: use of environment variables, command-line args, signals, STDOUT, STDERR
- Easy to run
- Development and Production parity
Embeds its own server
public static void main(final String[] args) {
// start jetty
logger.info("listening on port {}", port);
final ResoureConfig rc = ResourceConfig.forApplication(app);
final Server server = JettyHttpContainerFactory.createServer(baseUri, rc);
try {
server.start();
server.join();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- "Don’t deploy your application in Jetty, deploy Jetty in your application!"
- Integrates well with Jersey via
JettyHttpContainerFactory
- Jetty is not the only solution, but it’s mature and lightweight
Bundles its dependencies
$ gradle shadowJar
$ java -jar build/libs/my-app.jar
listening on port 8000
- Sometimes called "fat jar", "uber jar", or "shadow jar"
- Prefer all dependencies bundled together: we don't want dependencies to change after building
- Easier than using shell scripts to build up a classpath argument for java
- Include main class in jar manifest
Logs to STDOUT
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level %logger{5} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Easy debugging
- One stream of log statements with flexible downstream processing
- Let production environment worry about filtering, rotation
What does running a self-contained app look like?
systemd managed daemon
[Unit]
Description=my-app service
[Service]
EnvironmentFile=-/etc/sysconfig/my-app
ExecStart=/usr/bin/java \$JVM_OPTS -jar /opt/my-app/my-app-${version}-all.jar
User=my-app
Restart=on-failure
[Install]
WantedBy=multi-user.target
- Much easier than init scripts
- systemctl for start, stop, status, restart
- Automatic start on system boot
- Note: same command to start as used in development
don't forget journald
[vagrant@localhost ~]$ journalctl -fu how-to-microservice
-- Logs begin at Thu 2016-07-14 00:07:10 UTC. --
Jul 14 00:08:44 localhost.localdomain systemd[1]: Started how-to-microservice service.
Jul 14 00:08:44 localhost.localdomain systemd[1]: Starting how-to-microservice service...
Jul 14 00:08:44 localhost.localdomain java[11726]: 2016-07-14T00:08:44,718Z INFO c.j.App - listening on port 8000
- Captures STDOUT, STDERR from systemd managed daemons
- Easy to tail and grep your app's logs
- Highly configurable: log rotation, format, forwarding
What does deploying a self-contained app look like?
RPM
$ rpm -qlp build/distributions/how-to-microservice-0.0.4-1.e7.noarch.rpm
/etc/how-to-microservice
/etc/how-to-microservice/settings.conf
/etc/sysconfig
/etc/sysconfig/how-to-microservice
/etc/systemd
/etc/systemd/system
/etc/systemd/system/how-to-microservice.service
/opt/how-to-microservice
/opt/how-to-microservice/how-to-microservice-0.0.4-SNAPSHOT-all.jar
/opt/how-to-microservice/settings.conf
- Packages user install, config file template, systemd unit file, binaries, package dependencies (java)
- Host your own yum repository for easy updates (jenkins, Nexus)
- Establishes a common means for deploying your container-less apps
Build RPM with Gradle
task rpm(type: Rpm) {
it.dependsOn shadowJar
packageName = project.name
version = '0.0.4'
release = '1.e7'
os = LINUX
requires('java-1.8.0-openjdk')
...
- Netflix OSS nebula-ospackage-plugin gradle plugin
- Builds deb and rpm
- Host your own yum repository with Nexus, deploy with Gradle
Test RPM with Vagrant
Vagrant.configure(2) do |config|
config.vm.box = "geerlingguy/centos7"
...
config.vm.provision "shell", inline: <<-SHELL
sync_dir=/vagrant
rpm=($sync_dir/build/distributions/*.rpm)
if [ ! -f $rpm ]; then
echo "how-to-microservice RPM not found"
exit 1
fi
sudo yum erase -y how-to-microservice
sudo yum install -y $rpm
sudo systemctl restart how-to-microservice
SHELL
end
- Vagrant defines development virtual machines with Ruby DSL
- Vagrant shell provisioner installs RPM in the Vagrant virtual machine
-
Rebuild virtual machine:
vagrant destroy -f && vagrant up
What does dockerizing a self-contained app look like?
Dockerfile
FROM java:8
MAINTAINER Johnathan Gilday
COPY ./build/libs/how-to-microservice.jar /opt/how-to-microservice/
EXPOSE 8000
WORKDIR /opt/how-to-microservice
CMD ["java", "-jar", "how-to-microservice.jar"]
- Manage a docker container instead of a systemd service
- Manage logs with docker log driver instead of journald
- Deploy a docker image instead of an RPM
Thanks!
Sample self-contained jersey service
https://github.com/gilday/how-to-microservice
Gradle plugin for building "fat jars"
https://github.com/johnrengelman/shadow
Netflix OSS Gradle plugins (including gradle-os-package)
https://nebula-plugins.github.io/
Nexus yum repository hosting
https://books.sonatype.com/nexus-book/reference/yum-configuration.html
Vagrant shell provisioner
https://www.vagrantup.com/docs/provisioning/shell.html
Container-less Java services
By Johnathan Gilday
Container-less Java services
Development and deployment strategies for building container-less apps in Java inspired by lessons from the 12 Factor App, a methodology for building software-as-a-service apps for the cloud
- 3,129