Posts Tagged ‘netflix oss’


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.

Untitled (2)

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:

ragna_goodby-run

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: >
     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.

 

 

 

 

 

Advertisements