Spring Boot, Mongo and Docker. Run all with a single command.

In this article, we’ll se how to run a spring boot application with mongo db and a mongo client with a single command. For this purpose, we must dockerize our application, the mongo database and the mongo client.

Spring Boot Application

To create our Spring Boot application we use the project creation wizard provided by the Spring web.

SpringInitializr

Once the project is downloaded, we need to add the necessary configuration to dockerize the application and for the application to connect to Mongo.

How to dockerize the application.

We only need to add a plugin to the pom.xml file and our application will aready been dockerized.

 

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.3.4</version>
  <configuration>
    <repository>${docker.image.prefix}/${project.artifactId}&nbsp; </repository>
  </configuration>
  <executions>
    <execution>
      <id>default</id>
      <phase>install</phase>
      <goals>
        <goal>build</goal>
      </goals>
    </execution>
  </executions>
</plugin>
 

If we run mvn clean install we will see how our image has been created successfully.

MvnCleanInstall

How to connect our app to mongo db

First of all, we need to  add application.yml file. This file contains all configuration needed for our application.

spring.data.mongodb:
   database: customers # Database name.
   uri: mongodb://mongo:27017 # Mongo database URI. Cannot be set with host, port and credentials.

One of the first questions that would fit me would be:
Why does mongo appear as a host instead of localhost?

For one reason:

If we put localhost and run our application with docker, the mongo database won’t be founded. Mongo will be located in a container and our app will be located in a different container.

However, If we run our application with java, we only have to add following line to our /etc/hosts file.

mongo           127.0.0.1

Once the connection to mongo is configured, we need to add a repository to allow query database.

import org.davromalc.tutorial.model.Customer;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface CustomerRepository extends MongoRepository<Customer, String> {

}

Also, we need to enable mongo repositories in our main spring boot class.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@EnableMongoRepositories
@SpringBootApplication
public class Application {
   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
   }
}

Additionally, we can add a controller that returns all the data, in json format, from mongo or to be able to persist new data.

import java.util.List;

import org.davromalc.tutorial.model.Customer;
import org.davromalc.tutorial.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
public class CustomerRestController {

   @Autowired
   private CustomerRepository repository;

   @RequestMapping("customer/")
   public List findAll(){
      final List customers = repository.findAll();
      log.info("Fetching customers from database {}" , customers);
      return customers;
   }
@RequestMapping(value = "customer/" , method = RequestMethod.POST)
   public void save(@RequestBody Customer customer){
      log.info("Storing customer in database {}", customer);
      repository.save(customer);
   }
}

Our application is fine. We only have to create a docker image to be able to run a container with our application.

mvn clean install

Once the docker image is created, we can list all docker images and check if the docker image was created successfully.

DockerImageCreatedSuccessfully2

Besides, we can run a container from the image created

AppSuccessfullyRuned

At this point, we have our application dockerized but it cannot connect to mongo and we can’t see mongo data.

Mongo import

So that we can have data every time we start our mongo db, I have developed a script that persists in mongo the json files that we enter inside the folder mongo-init.

The script does the following:

  1. For each folder inside the data-import folder, it reads all json files and creates a database with the name of the folder.
  2. For each json file, it creates a collection with the name of the file and persists all json objects contained in the file.

When we run this script, we can check if data is persited searching in logs.

MongoInitLog1MongoInitLog2

Note: The json file must contain an array of objects in json format.

Dockerized mongo

So that we do not have to have installed mongo in our laptop, we can dockerize mongo and only use it when it is necessary and to be able to export all our technology stack easily.

We only have to search docker image and pull from the repository.

docker pull mongo:latest

DockerPullImage

Mongo client

Mongo Client is a web interface that allows us to visualize in a simple way the contents of our collections.

We only have to search docker image and pull from the repository.

We do not need to pull the image, when we run the docker compose this will download all the images that are not founded in our local repository.

Run all with a single command.

If we wanted to run our application, with mongodb, with the imported data and with mongo client we would need to execute the 4 containers in the following order:

  1. Mongo
  2. Mongo Import
  3. Mongo Client
  4. App

Running 4 commands every time we make a change is not very useful and often causes bugs.

To solve this problem, we have the docker compose tool that allows us to execute several containers of the same stack and create links between the different containers so that they have visibility between them.

version: "2.1"
   services:
      mongo:
         image: mongo:3.2.4
         ports:
            - 27017:27017
         command: --smallfiles

      mongo-init:
         build: ./mongo-init
         links:
            - mongo

      mongo-client:
         image: mongoclient/mongoclient
         ports:
            - 3000:3000
         environment:
            - MONGOCLIENT_DEFAULT_CONNECTION_URL=mongodb://mongo:27017
         links:
            - mongo

# APP ************************************************************
      spring-boot-mongo-docker:
         image: davromalc/spring-boot-mongo-docker
         ports:
            - 8080:8080
         links:
            - mongo
         entrypoint: "java -Djava.security.egd=file:/dev/./urandom -jar /app.jar"

We are going to inspect the contents of this file:

  1. Services means each container we will run
  2. Mongo: We are going to use mongo image and expose its tipically port: 27017
  3. Mongo Init:  Build from Dockerfile . Copy the script into the container and run it. It have a link with mongo container. This link is very importante so if this link does not exist, the data would not persist in the desired mongo.
  4. Mongo Client:  From latest docker i :Dmage. The url of the mongo to which it accedes must be configured through environment. As in the application.yml file we must indicate the url with «mongo» and not with «localhost». And why mongo? Because mongo is the name of the mongo service declared in line 3. If in line 3 we would have declared the service as my-mongo-database, in this environment variable we should put: mongodb://my-mongo-database:27017 and the same in the application.yml file.
  5. App: From the image created above. We expose 8080 port. We set a link with mongo. If we doesn’t set this link, our app would nott run.

At this point, we can run the 4 containers with a single command, as we promised at the beginning of this article. 😀

We should to run the following command:

docker-compose -up

 

Each container has been executed successfully and we  can’t see errors in logs.

Note: If we modify the dataset of mongo init, we hace yo re-build the docker-image. For that purpouse, we hace to run the following command:

docker-compose up --build

Now, We can check if all configuration is fine and if we can access mongo data from mongo client and from our application.

Mongo Client:

Our Application:

AppGetCustomers

Calling Rest Controller

AppGetData2Logs output

AppCreateCustomer1

Create customer with postman tool

AppCreateCustomer2

Logs output

AppCreateCustomer3

Now, we can check if customer has been persisted successfully.

Conclusion

Thanks to the docker-compose tool we can build a poweful technological stack that helps us develop and build spring boot applications in a simple and intuitive way.

Mongo client and mongo import data are two useful tools for development that allow us to speed up the development of our application and have an clean dataset every time we run the application. This is very helpful to run the integration tests for example.

The full source code for this article is available over on GitHub.

 

Integración continua con Jenkins Pipeline

Desde principios de 2017, estoy inculcando en mi empresa la importancia y la necesidad de tener un entorno de CI consolidado. Empecé instalando un Jenkins en un pequeño servidor perdido y ahora mismo he conseguido un proyecto para implantar integración continúa dentro de todos los equipos de desarrollo que hay.

En este post me gustaría plasmar muchos de los conocimientos adquiridos.

Definición del Workflow

En primer lugar, hemos definido un Workflow por el que tendrán que pasar todos los proyectos para asegurarnos en todo momento  que esos proyectos cuentan con la calidad necesaria y además para realizar despliegues automáticos en pre-producción y producción.

Os muestro un esquema general del workflow propuesto:

 

Workflow

Como resumen rápido, los pasos del workflow serían:

  1. Descargar el código
  2. Compilar
  3. Ejecutar los tests
  4. Realizar un análisis de calidad de código, comprobar la cobertura y análisis de seguridad.
  5. Despliegue en pre-producción
  6. Confirmación por parte humana.
  7. Realizar un tag del código
  8. Despliegue en producción
  9. Notificar cambios en JIRA

Implementación del Workflow

Para implementar este workflow de integración continúa, creo que la herramienta Jenkins en su versión 2 es la herramienta perfecta para ello ya que permite crear «Pipelines» o workflows con ficheros en lenguaje groovy con todas las ventajas que eso tiene.

Así que me puse manos a la obra y empecé a trabajar con Jenkins Pipeline  no sin darme algún que otro quebradero de cabeza.

Elegí Maven como herramienta principal ya que con diferentes plugins me permite realizar la mayoría de las acciones del workflow. También podría haber utilizado los plugins de Jenkins pero no todos están actualizados para dar soporte de pipeline.

Adjunto pipeline desarrollado.

#!groovy

pipeline {
     agent any    //Agente de Docker, de momento no utilizo Docker
     tools { //Alias a herramientas instaladas en Jenkins
        maven 'M3' //M3 es el nombre que le puse al maven instalado para Jenkins
        jdk 'JDK8' //JDK8 es el nombre que le puse al java de Jenkins
    }
    options {
        //Si en 3 días no ha terminado que falle.
        timeout(time: 76, unit: 'HOURS') 
    }
    environment {
        //variable con el nombre del proyecto
        APP_NAME = 'My-App'
    }
    stages { //Inicio fases del workflow
       stage ('Initialize') { //Primer paso, notificar inicio workflow
             steps {
                  slackSend (message: 'Inicio ejecucion ' + APP_NAME, channel: '#jenkins', color: '#0000FF', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXX' )
                  hipchatSend (color: 'GRAY', failOnError: true, notify: true, message: 'Inicio ejecucion ' + APP_NAME + ' <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', v2enabled: true,  room: 'Jenkins' )
            }
       }
       stage ('Build') { //Compilamos el proyecto
            steps {
                 bat "mvn -T 4 -B --batch-mode -V -U -e -Dmaven.test.failure.ignore clean package -Dmaven.test.skip=true"
            }
       }
       stage ('Test') {
            //Fase de tests. En paralelo tests automaticos y de rendimiento
            steps {
                 parallel 'Integration & Unit Tests': {
                     bat "mvn -T 4 -B --batch-mode -V -U -e test"
                 }, 'Performance Test': {
                     bat "mvn jmeter:jmeter"
                 }
           }
       }
       stage ('QA') {
       //Fase de QA. En paralelo Sonar, Cobertura y OWASP
           steps {
                parallel 'Sonarqube Analysis': {
                    //Si quieres ver la cobertura en sonar es necesario ejecutar cobertura y después sonar
                    bat "mvn -B --batch-mode -V -U -e org.jacoco:jacoco-maven-plugin:prepare-agent install -Dmaven.test.failure.ignore=true"
                    bat "mvn -B --batch-mode -V -U -e sonar:sonar"
                    echo 'Sonarqube Analysis'
               }, 'Cobertura code coverage' : {
                    //Realizamos análisis de cobertura de código
                    //Si la cobertura de código es inferior al 80% falla la ejecución y falla el workflow
                    bat "mvn -B --batch-mode -V -U -e verify"
               }, 'OWASP Analysis' : {
                    bat "mvn -B -X --batch-mode -V -U -e dependency-check:check"
               }
          }
          //Tras ejecutar los pasos en paralelos guardo el reporte de tests
          post {
               success {
                    junit 'target/surefire-reports/**/*.xml' 
               }
          }
      }
      stage ('Deploy to Pre-production environment') {
      //Desplegamos en el entorno de Pre-Producción
      //Se despliega en un tomcat con el plugin Cargo
           steps {
                bat "mvn -B -P Desarrollo --batch-mode -V -U -e clean package cargo:redeploy -Dmaven.test.skip=true"
           }
      }
      stage ('Confirmation') {
      //En esta fase esperamos hasta que la persona configurada confirme que desea subir a Producción. 
      //Tiene 72 horas para confirmar la subida a Producción.
      //Se envían notificaciones para que la persona tenga constancia
           steps {
                slackSend channel: '@dromeroa',color: '#00FF00', message: '\u00BFDeseas subir a produccion?. \n Confirma en la siguiente web: ${BLUE_OCEAN_URL}' , teamDomain: 'my-company', token: 'XXXXXXXXXXX'
                hipchatSend (color: 'YELLOW', failOnError: true, notify: true, message: '\u00BFDeseas subir a producci\u00F3n\u003F. \n Confirma en el siguiente <a href="${BLUE_OCEAN_URL}">Enlace</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
                timeout(time: 72, unit: 'HOURS') {
                    input '\u00BFContinuar con despliegue en producci\u00F3n\u003F'
                }
           }
      }
      stage ('Tagging the release candidate') {
           //Realizamos un tag en SVN del código fuente
           steps {
               //Tagging from trunk to tag
               echo "Tagging the release Candidate";
               bat "mvn -B --batch-mode -V -U -e scm:tag -Dmaven.test.skip=true"
          }
      }
      stage ('Deploy to Production environment') {
           //Comenzamos a subir a producción en los dos servidores 
           steps {
                parallel 'Server 1': {
                    //Necesitamos realizar reintentos ya que falla la subida en remoto y se producen colisiones
                    retry(6) {
                        bat "mvn -T 4 -B -P Produccion --batch-mode -V -U -e tomcat7:redeploy -Dmaven.test.skip=true"
                    }
                }, 'Server 2' : {
                    retry(6) {
                        bat "mvn -T 4 -B -P Produccion --batch-mode -V -U -e tomcat:redeploy -Dmaven.test.skip=true"
                    }
                }
           }
      }
      stage ('CleanUp') {
      //Limpiamos el workspace para no llenar los discos
           steps {
                deleteDir()
           }
      }
    } //Fin de las fases del workflow
    //Inicio de acciones post ejecución del workflow
    //Notificamos como ha sido la ejecución del workflow
    post {
      success {
           slackSend channel: '#jenkins',color: '#00FF00', message: APP_NAME + ' ejecutado satisfactoriamente.', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXXX'
           hipchatSend (color: 'GREEN', failOnError: true, notify: true, message: APP_NAME + ' ejecutado satisfactoriamente. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
      failure {
           slackSend channel: '#jenkins',color: '#FF0000', message: APP_NAME + ' se encuentra en estado fallido. ${BLUE_OCEAN_URL}', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXX'
           hipchatSend (color: 'RED', failOnError: true, notify: true, message: APP_NAME + ' se encuentra en estado fallido. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
      unstable {
           slackSend channel: '#jenkins',color: '#FFFF00', message: APP_NAME + ' se encuentra en estado inestable. ${BLUE_OCEAN_URL}', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXXX'
           hipchatSend (color: 'RED', failOnError: true, notify: true, message: APP_NAME + ' se encuentra en estado inestable. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
    }
   }

Se puede comprobar que el workflow permite automatizar un montón de tareas que el desarrollador tendría que realizar en lugar de dedicarse a desarrollar, y además dichas tareas son susceptibles a fallos, con lo cuál eliminamos esa posibilidad.  También podemos imponer una serie de reglas de calidad en el código que han de ser cumplidas si se quiere realizar un despliegue de una nueva versión.

A continuación, muestro como se ve en Jenkins una ejecución del Workflow en Jenkins. Nuestro Jenkins tiene instalado el plugin Blue Ocean que cambia la interfaz de Jenkins.

Workflow2

Próximos pasos:

  • Integrar con Docker
  • Integrar con Ansible para realizar puestas en producción de Artefactos JAR
  • Integrar con JIRA para notificar al cliente que se ha desplegado una nueva versión.

En próximos artículos mostraré como han ido las próximas integraciones y como realizarlas.

Este post muestra una visión rápida y a «grosso modo» de como se ha realizado el proceso de instaurar una política de integración continua. Quedarían detalles por explicar como la configuración de los plugins de Maven o la instalación de Jenkins.

Finalizo este post invitando a cualquier persona que tenga alguna duda a dejar un comentario para que pueda ayudar en la medida de lo posible. 🙂

Enlaces de interés: