In last installment, we place the topology and basic stack for our riddance from Java EE monolith. We coded an infrastructure in Ansible upon a Vagrant VM simple topology as follows.
We created four machines: hello-service1 and hello-service2, for our microservice, and, proxy-active and proxy-bkp for our load balancer. Now we’ll dig in our simple Spring Boot service and then we’ll provision the microservice machine with java and a standard distribution package for Debian Linux family using Netflix Nebula os-packager plugin for Gradle.
This post wasn in my original plan, but the Linux standard distribution scheme showed a high importance in overal microservices deployment scheme.
A simple Spring Boot Rest Service
We have plenty of examples using Spring Boot Microservices. So we’ll limit it to the basics once that we’re interested in the infrastructure concerns around spring boot microservices. I used as base the Spring Boot Guide for Rest Service, so, please, refer to it for a more comprehensive explanation. Our goodbye service is in github. Here follows its main snippet:
@RestController @ConfigurationProperties(prefix = "goodbye") public class GoodbyeController { private static final String template = "[%s] Goodbye JavaEE Monolith"; private final AtomicLong counter = new AtomicLong(); @Value("${ragna.gooodbye.instance:'NO_INSTANCE_SET'}") private String instanceId; @RequestMapping("/goodbye") public Goodbye goodbye (@RequestParam(value="name", defaultValue = "default node") String name){ return new Goodbye(instanceId, counter.incrementAndGet(), String.format(template, name)); } }
The interesting part for us is the use of the @Value annotation on instanceId attribute, that takes a value from a java property named ragna.goodbye.instance.
Executable jar with Spring Boot Plugin for Gradle
The interesting thing here in our Spring Boot Service is the the use of the Spring Boot Plugin for Gradle in order to create an executable jar. Let’s highlight the build.gradle important setup for its generation:
buildscript { ext { } repositories { } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath "com.netflix.nebula:gradle-ospackage-plugin:${osPackageVersion}"; } } apply plugin: 'spring-boot' apply plugin: 'nebula.ospackage' group = 'ragna' version = '0.1.0' mainClassName = 'ragna.goodbye.Application' dependencies { compile("org.springframework.boot:spring-boot-starter-web") testCompile("junit:junit") } distributions { main { baseName = 'ragna-goodbye' version = "${project.version}" } } jar { baseName = 'ragna-goodbye' version = "${project.version}" manifest { attributes("Implementation-Title": "Ragna Service, "Implementation-Version": "${project.version}") } } springBoot { executable = true excludeDevtools = true }
First, we can see above the buildscript dependencies refering to Netflix Nebula and Spring Boot. We tell to Gradle that we are customizing the build by applying the plugins, as we can see it for ‘spring-boot’ and ‘nebula-osspackage’ plugin names.
We customize the baseName for distributions plugin and the jar plugin (both implicitly imported). This will define the name for generated zip, tar and jar packages.
Finally, in the customization for Spring Boot, we tell to this plugin that we want a executable jar. This plugin will instrument the final jar so that we could run it without the the explicit call for the java runtime, as follows:
This facilitates the administration of services in linux environments.
Building a Debian package with Netflix OSS – Nebula osspackage
Each Linux family, Debian, CentOS presents several differences as packaging system. Here we’ll focus in Debian Packaging System, used by its derivatives such ubuntu and mint, too.
The systemd provides for us an standard for service administration. To comply with the standard, we need setup specific users for the service, initialization and termination scripts, runlevel and so on. To create a debian package we’ll customize our Netflix Nebula ospackage as follows:
ospackage { packageName = 'ragna-goodbye' version = "${project.version}" release = '1' type = BINARY os = LINUX preInstall file("scripts/rpm/preInstall.sh") postInstall file("scripts/rpm/postInstall.sh") preUninstall file("scripts/rpm/preUninstall.sh") postUninstall file("scripts/rpm/postUninstall.sh") into "/opt/local/ragna-goodbye" user "ragna-service" permissionGroup "ragna-service&" from(jar.outputs.files) { // Strip the version from the jar filename rename { String fileName -> fileName.replace("-${project.version}", "") } fileMode 0500 into "bin" } from("install/linux/conf") { fileType CONFIG | NOREPLACE fileMode 0754 into "conf" } }
Firstly we define the package name, os and type. To install the service, we need to provide custom bash scripts for the following installation events: preInstall, which we’ll use to create custom linux user and group, postInstall, used for change log directory ownership to our user; preUninstall, used to stops the service previously its removal and our unused (onde that our service is simple) postUninstall script. The scripts are placed in the scripts/rpm directory of our application. Here are the snippets:
preInstall.sh:
#!/usr/bin/env bash echo "Creating group: ragna-service" /usr/sbin/groupadd -f -r ragna-service 2> /dev/null || : echo "Creating user: ragna-service" /usr/sbin/useradd -r -m -c "ragna-service user" ragna-service -g ragna-service 2> /dev/null || :
postInstall.sh:
#!/usr/bin/env bash chown ragna-service:ragna-service /opt/local/ragna-goodbye/log
preUninstall.sh:
#!/usr/bin/env bash service ragna-goodbye stop
postUninstall.sh:
#!/usr/bin/env bash # Nothing here...
Now we set the placement of our jar packaged microservice to /opt/local/ragna-goodbye, via into attribute along with the user and permissiontGroup. In the bin target directory, we copy the fat jar built by spring boot plugin.
Then we copy, paying attention in the needed file permissions, the configuration files in the conf target folder.
We need two files, the file used to set the bootstrap parameters in bash for the java service ragna-goodbye.conf and the properties file for the spring boot service ragna-goodbye.properties. Is noteworthy that we define the fileType for both telling to debian that we don’t want it to be replaced in the case of a new installation, once that it must contain custom properties for the specific machine. It follows:
ragna-goodbye.conf:
# The name of the folder to put log files in (/var/log by default). LOG_FOLDER=/opt/local/ragna-goodbye/log # The arguments to pass to the program (the Spring Boot app). RUN_ARGS=--spring.config.location=file:/opt/local/ragna-goodbye/conf/ragna-goodbye.properties
ragnar-goodbye.properties:
server.port: 9000 server,address: 0.0.0.0 management.port: 9001 management.address: 127.0.0.1
Back to the gradle.build file, we define the creation of the Debian package (remember, nebula oss package builds rpm, too):
</pre> <pre> buildDeb { user "ragna-service" permissionGroup "ragna-service" directory("/opt/local/ragna-goodbye/log", 0755) link("/etc/init.d/ragna-goodbye", "/opt/local/ragna-goodbye/bin/ragna-goodbye.jar") link("/opt/local/ragna-goodbye/bin/ragna-goodbye.conf", "/opt/local/ragna-goodbye/conf/ragna-goodbye.conf") }</pre> <pre>
In Debian packaging we setup the user and permissionGroup for the service, the log directory and the link for our jar packaged service in the linux init system. Here we see how handy the Spring Boot executable jar can be. Finally we set a link for the ragna-goodbye.conf in the bin folder, as Spring needs to find it in the same folder of the jar service.
To build the debian package we must issue in our project folder:
$ gradle clean build buildDeb
A tip. It’s important to pay attention in the generated pacakges, deb, jar, names and the names used in scripts. At this moment we don’t have any validation between jar, deb names and the scripts used to manage installation and uninstallation.
I misspelled the service name in the preUnintall script and had to manually edit the package name in the installed script to properly stop the service before issuing sudo apt-get remove. To find it I use the following snippet:
$ sudo find /var | grep ragna-goodbye /var/lib/dpkg/info/ragna-goodbye.list /var/lib/dpkg/info/ragna-goodbye.postinst /var/lib/dpkg/info/ragna-goodbye.md5sums /var/lib/dpkg/info/ragna-goodbye.prerm /var/lib/dpkg/info/ragna-goodbye.postrm /var/lib/dpkg/info/ragna-goodbye.preinst
Refactoring Java Provisioning using Ansible roles
In last post we provisioned oracle 8 java in our microservice machine using ansible. Now it’s time to install the debian package containing the Spring Boot service int the microservice machine.
Before provision our deb package, we’ll refactor the previous provisioning for our Machine using Ansible Roles. Roles are a modularization mechanism for ansible that organizes features like tasks, vars and handlers in a known file structure. We’ll create a role for the java 8 installation tasks included in our microservices.yml file.
First we’ll create the following file structure inside the provisioning directory in our Vagrant project.
vagrant_spring_boot_ha - roles - jdk8 - tasks
We can create by issuing the following command in vagrant_spring_boot_ha folter:
$ mkdir -p provisioning/roles/jdk8/tasks
In the tasks folder we’ll create a main.yml file with the contents from the tasks block from microservices.yml, as follows:
- block: - name: Install Oracle Java 8 repository apt_repository: repo='ppa:webupd8team/java' - name: Accept Java 8 License debconf: &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; name='oracle-java8-installer' question='shared/accepted-oracle-license-v1-1' value='true' vtype='select' - name: Install Oracle Java 8 apt: name=oracle-java8-installer update_cache=yes state=present force=yes
In the microservices.yml file we remove the jdk installation commands and include a new roles directive with a sub-item pointing to jdk8, the name of our new role. The file new content follows:
--- - hosts: spring-boot-microservices tasks: - debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}" roles: - jdk8
Despite the location of the role clause in the file, the jdk8 role will provisioned before the execution of any tasks defined in the playbook file.
Provisioning our microservice as a Debian package
To keep the solution simple, I’m keeping aside two important elements for our solution. Jenkins, as build pipeline automation tool, and Sonatype Nexus. I’ll provide the deb package trough github.
We’ll create a new role named ragna-packages with the following command:
$ mkdir -p provisioning/roles/ragna-packages/tasks
There we create the following main.yml file that installs the ragna-goodbye service using the ansible apt module. To provide a full runnable example, I deployed the deb file directly from github. As you can see we have several ansible variables delimited by “{{” and “}}”, some of them are provided by ansible facts environment gathering, some will be provided by us. It follows our package provisioning role.
--- - name: download '{{ package_repo }}/{{ package_name }}' get_url: url={{ package_repo }}/{{ package_name }} dest=/tmp/{{ package_name }} mode=0440 - name: install '{{ package_name }}' service from '{{ package_repo }}' apt: deb=/tmp/{{ package_name }} - name: placing instance name '{{ inventory_hostname }}' in file '{{ package_repo }}' lineinfile: dest={{ conf_file }} line="ragna.gooodbye.instance:{{ inventory_hostname }}" notify: restart {{ service_name }}
Above, we download the debian package we created using nebula using the get_url module from ansible. The downloaded debian package is installed by apt module and finally we customize the configuration properties file used by the service using the lineinfile ansible module that will add the property ragna.goodbye.instance filled with the inventory_name ansible fact.
The notify clause in the above script is a handler. A handler is an abstraction for service lifecycle handling. The handler for the ragna-service is placed in the file main.yml from directory provisining/ragna-packages/handlers, that we can create as we did for the tasks file before.
--- - name: restart {{ service_name }} service: name={{ service_name }} state=restarted enabled=yes
The notify clause from linefile task refers to the name for the service handler ‘ restart {{ service_name }}’. This handler, will be notified to restart the service after the configuration property update.
Now we update our microservices.yml, adding the new role with the required parameters, package_repo, package_name and service_name:
--- - hosts: spring-boot-microservices tasks: - debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}" roles: - jdk8 - { role: ragna-packages, package_repo: "http://rawgit.com/ragnarokkrr/rgn_vm_containers/master/vagrant_springboot_ha/provisioning", package_name: "ragna-goodbye_0.1.0-1_all.deb", conf_file: "/opt/local/ragna-goodbye/conf/ragna-goodbye.properties", service_name: "ragna-goodbye" }
As you can notice, the ragna-packages role can be reused for any similar developed Spring Boot services provisioned as debian packages. I omitted some important steps needed In a real-world deploy pipeline performed by Jenkins and OSS Nexus. But this gap could be filled by a little googling.
Running
To run our project, we go to the vagrant_springboot_ha directory in the host machine and type:
$ vagrant destroy $ vagrant up hello-service1 $ vagrant ssh hello-service1
Logged in the hello-service1 machine we can download the service using wget localhost:9000/goodbye that will save a file named goodbye with the following json content:
{"instanceId":"hello-service1", "id":1, "content":"Goodbye JavaEE Monolith"}
As you can see, the insanceId is updated with the machine name given by Vagrant Multimachine. Here follows the final ragna-goodbye.properties file modified by ansible and placed in the /opt/local/ragna-goodbye/conf/ direcotry, as specified in nebula:
server.port: 9000 server,address: 0.0.0.0 management.port: 9001 management.address: 127.0.0.1 ragna.gooodbye.instance:hello-service1
Concluding…
We saw how to provide a microservice that leverages the standard Linux services and administrative features. This is important as we don’t need to rely anymore in proprietary administration tools and GUI’s from traditional Java EE application servers and establishes the Linux as a new common and really standardized platform for deployment and administration for java applications.