1. Überblick

In diesem Artikel erstellen wir eine Continuous Integration Pipeline mit Jenkins. Wir lassen den Jenkins lokal laufen und bauen die wesentlichen Schritte für Continuous Integration ein: Spring Boot Anwendung aus GitHub auschecken, kompilieren, testen, Docker Image bauen, Docker Image ins DockerHub pushen.

2. Jenkins im Docker Container starten

Das Starten des Jenkins in einem Docker Container ist sehr einfach, denn hierfür stellt Jenkins ein offizielles Docker Image im Docker Hub bereit: https://hub.docker.com/r/jenkins/jenkins

Um den Jenkins im Container zu starten, führen wir folgenden Befehl aus:

docker run -p 8080:8080 -p 50000:50000 --privileged -u root  -v jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v /usr/local/bin/docker:/usr/local/bin/docker jenkins/jenkins:lts

Damit ist der Jenkins lokal unter der URL http://localhost:8080/ zu erreichen. Unter dem Ordner /var/jenkins_home speichert Jenkins seine Daten ab, unter anderem die installierten Plugins und Konfigurationen. Hierfür geben wir dem Container ein eigenes Volume damit unsere Installation auch ein Neustart überlebt.

Wenn der Container erfolgreich gestartet ist, was beim ersten Mal etwas Dauern kann, sehen wir zum Ende des Logs ein Passwort. Dieses Passwort benötigen wir bei der initialen Installation, um den Jenkins zu entsperren.

Das Passwort befindet sich unter /var/jenkins_home/secrets/initialAdminPassword.  

Anschließend werden wir von Jenkins durch die Installation geführt, wir installieren die Default Plugins und legen zum Schluss den Benutzer an.

3. Konfiguration der Hilfsprogramme: Maven und Docker

In unserer Pipeline möchten wir Maven und Docker benutzen um unsere Anwendung zu bauen. Um diese Hilfsprogramme im Jenkins zu konfigurieren, gehen wir auf Jenkins > Jenkins verwalten > Konfiguration Hilfsprogramme. Unter den Abschnitten Maven und Docker können wir eine automatische Installation der Tools einstellen, siehe nächster Screenshot.

4. JENKINS Pipeline erstellen

Jenkins ist nun lokal installiert und konfiguriert, jetzt können wir beginnen unsere Pipeline deklarativ aufzubauen. 

Im ersten Schritt soll unsere Jenkins Pipeline folgendes können:

  1. Code auschecken
  2. Code kompilieren
  3. Tests ausführen

Wir bauen die Pipeline für die bestehende Beispielanwendung https://github.com/softwarehandwerk/sample-docker-java-app, hierbei erweitern wir das Projekt um das folgende Jenkinsfile: 

Jenkinsfile

pipeline {
   agent any

   environment {
      dockerHome = tool 'JenkinsDocker'
      mavenHome = tool 'JenkinsMaven'
      PATH = "$dockerHome/bin:$mavenHome/bin:$PATH"
   }

   stages{

      stage('Compile') {
         steps{
            sh "mvn clean compile"
         }
      }

      stage('Test') {
         steps{
            sh "mvn test"
         }
      }

      stage('Integration Test') {
         steps{
            sh "mvn failsafe:integration-test failsafe:verify"
         }
      }

      stage('Package') {
         steps{
            script{
               sh "mvn package -DskipTests"
            }
         }
      }
   }
}

In der Direktive „environment“ konfigurieren wir unsere installierten Versionen von Maven und Docker. Unter „stages“ fügen wir drei Stages hinzu, in denen wir das Projekt mit Maven kompilieren, die Tests ausführen und anschließend paketieren. Das Auschecken des Source Codes muss nicht explizit in einer Stage deklariert werden, dies erfolgt automatisch durch die Pipeline Direktive.

Als nächstes erstellen wir im Jenkins ein neues Job Element (Pipeline) und nennen es „sample-docker-java-app-pipeline“. Nach dem Anlegen gelangen wir auf die Konfigurationsseite des Jobs, hier konfigurieren wir wie der Jenkins-Job an unsere Jenkinsfile kommt, siehe Screenshot.

Das Repository mit unserer Jenkinsfile ist öffentlich auf GitHub https://github.com/softwarehandwerk/sample-docker-java-app.git , daher benötigen wir auch keine Credentials zu konfigurieren um darauf zuzugreifen. Damit wäre auch die Konfiguration des Jobs an dieser Stelle fertig und wir können den Job starten.

5. Docker Image bauen und pushen

Unsere Anwendung ist nun gebaut, getestet und liegt bereit zum Paketieren unter dem target Ordner. Das Dockerfile welches direkt neben der pom.xml liegt, basiert auf einem schmalen Alpine Image. Im „COPY“ Befehl wird unsere Anwendung auf die oberste Ebene kopiert damit die Anwendung nach dem Start des Container mit dem CMD Befehl gestartet werden kann.

Dockerfile

FROM openjdk:8-jre-alpine
COPY target/*.jar /app.jar
EXPOSE 8080
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", "/app.jar"]

In der Pipeline greifen wir auf unser bereits deklariertes Tool Docker zu und rufen die build Methode mit dem Repositorynamen und dem Buildtag auf. Nach dem Ausführen von dieser Stage wurde das Docker Image gebaut und in das lokale Repository abgelegt. 

stage('Build Docker Image') {
    steps{
        script{
            dockerImage = docker.build("softwarehandwerk/sample-docker-java-app:${env.BUILD_TAG}")
        }
    }
}

Bevor wir im letzten Schritt unser Docker Image nach Docker Hub pushen, müssen wir in Jenkins die Zugangsberechtigungen für Docker Hub hinterlegen. Unter Jenkins > Zugangsdaten > System > Globale Zugangsdaten legen wir die Zugangsdaten ab.

Sobald die Zugangsdaten für Docker Hub in Jenkins hinterlegt sind, können wir die letzte Stage „Push Docker Image“ in unsere Pipeline einbauen. Das Docker Pipeline Plugin bietet mit der Methode withRegistry die Möglichkeit sich mit dem Registry vom Docker Hub zu verbinden, als Parameter bekommt es die Jenkins Credentials ID. 

stage('Push Docker Image') {
    steps{
        script{
            docker.withRegistry('',’dockerhub-credentials-id'){
                dockerImage.push()
                dockerImage.push('latest')
            }
        }
    }
}

Wenn das Pushen aus der Pipeline erfolgreich war, dann ist das Docker Image im Docker Hub sichtbar.

6. FAZIT

In diesem Artikel haben wir eine Jenkins Pipeline mit den wesentlichen Schritten für Continuous Integration gebaut. Dabei haben wir gesehen wie einfach es sein kann mit Hilfe von Docker den Jenkins lokal zu starten und die wichtigsten Pipelineschritte einzubauen. Wir können nun in einem lokalen Jenkins ein Projekt aus GitHub auschecken, dieses bauen und testen, anschließend ein Docker Image bauen und es in DockerHub einpflegen.

Der nächste Schritt wäre „Delivery“.

https://github.com/softwarehandwerk/sample-docker-java-app/tree/pipeline

Source Code auf GitHub

Read More
Erstellen einer Jenkins Pipeline zum Bauen und Pushen von Docker Images

1. Überblick

In diesem Artikel schauen wir uns an, wie man die Kommunikation zwischen Microservices nachverfolgen kann. Mit Spring Cloud Sleuth erweitern wir das Logging um Span IDs und stellen diese anschließend mit Jaeger UI grafisch dar.

Im Artikel Kommunikation von Microservices auf OpenShift – REST mit OpenFeign haben wir drei Microservices, die miteinander über REST kommunizieren, entwickelt. Dieses Projekt werden wir im Rahmen dieses Artikels erweitern, um das verteilte Tracing zu demonstrieren.

2. Installation von Jaeger als All-in-One Lösung für OpenShift

Jaeger ist ein Monitoring-System für komplexe verteilte Anwendungssysteme. Es ist sehr einfach den gesamten Stack auf OpenShift mit folgendem oc-Befehl zu installieren:

oc process -f https://raw.githubusercontent.com/jaegertracing/
jaeger-openshift/master/all-in-one/jaeger-all-in-one-template.yml 
| oc create -f -

Nach erfolgreicher Installation sehen wir in der OpenShift Konsole den gesamten Stack der All-in-One Lösung. Siehe nächste Abbildung.

Die Installation über das OpenShift Template ist für Beispielanwendungen sehr komfortabel, dies ist aber nicht für den Produktiveinsatz geeignet, da bei dieser Installation unter anderem keine Persistenz eingerichtet wird.

Für uns ist der Port und der Servicename von Zipkin entscheidend http://zipkin:9411. Im nächsten Schritt werden wir Spring Sleuth auf diese URL konfigurieren, damit Tracinginformationen bereitgestellt werden können.

3. Spring Cloud Sleuth und Spring Cloud Zipkin Dependencies einbinden

Die Kernfunktionalität von Spring Cloud Sleuth ist die Bereitstellung von zusätzlichen Span-Informationen für jeden Request. Diese zusätzliche Information wird zum einen im Logging sichtbar, zum anderen besteht die Möglichkeit die Span-Information direkt an ein Distributed Tracing System wie z.B. Zipkin zu schicken. Zipkin sammelt diese Informationen und stellt bequeme Lookup Funktionalitäten bereit, die wir uns später anschauen.

Jeder Microservice wird um zwei neue Dependencies erweitert:

  • spring-cloud-starter-sleuth
  • spring-cloud-starter-zipkin

pom.xml

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-sleuth</artifactId>
			<version>2.2.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zipkin</artifactId>
			<version>2.2.1.RELEASE</version>
		</dependency>

Mit der zweiten Dependency spring-cloud-starter-zipkin erkennt Spring Boot durch Autocofiguration, dass es die Span-Informationen an den Zipkin Server schicken muss. Die URL des Zipkin Servers tragen wir in unsere application.properties ein:

application.properties

spring.zipkin.baseUrl: http://zipkin:9411/

Mit diesen Anpassungen haben wir das Tracing bereits aktiviert. Im nächsten Abschnitt zeigen wir, wie die Span-Informationen grafisch in Jaeger UI dargestellt werden.

4. Distributed Tracing von Aufrufen

Um etwas Traffic zwischen den Microservices zu verursachen, rufen wir order-service auf folgendem REST Endpoint /orders/3 auf. Hierbei wird implizit customer-service aufgerufen, um die Kundeninformationen zu bekommen und es werden mehrere Aufrufe vom product-service für die Produktinformationen getätigt.

Ein Aufruf verursacht damit ziemlich viel Traffic, was wir auf dem Jaeger Dashboard sehen können, siehe nächste Abbildung.

Jaeger UI präsentiert uns den kompletten Trafficverlauf ziemlich übersichtlich. Wir sehen anhand der Abbildung, dass es nur ein Aufruf vom customer-service gibt und vier Aufrufe vom product-service. Wir sehen ebenfalls, wie lange jeder Aufruf gedauert hat und in welcher Reihenfolge diese ausgeführt wurden.

Wir können auf die einzelnen Services klicken und erhalten nähere Information zu jedem Request:

  • IP Adresse
  • http.mehtod
  • http.path
  • mvc.controller.class
  • mvc.controller.method
  • peer.ipv4

5. Fazit

Spring Boot macht wie immer die Magie. Zwei neue Dependencies, eine Anpassung der application.yaml und schon senden unsere Microservices Tracinginformationen an ein Monitoring-Tool.

Spring Cloud Sleuth erhebt die Daten und sendet es an Zipkin. Jaeger UI bereitet für uns die verteilten Kommunikationsdaten intuitiv auf und hilft uns damit eine bessere Übersicht über unsere verteilte Anwendung zu bekommen. Bei größeren Microservices-Architekturen kommt man über solch ein Monitoring-Tool nicht herum.

Das Projekt findet ihr in GitHub unter dem branch sample-openshift-sleuth-jaeger

Read More
Distributed Tracing mit Spring Cloud Sleuth und Jaeger auf OpenShift

1. Überblick

In diesem Artikel schauen wir uns an, wie mehrere Microservices untereinander über REST auf OpenShift kommunizieren. Damit wir die Kommunikation und die Konzepte Load Balancing sowie Service Discovery auf OpenShift demonstrieren können, entwickeln wir im Rahmen dieses Artikels drei Microservices, die über deklarative REST Clients miteinander integriert werden.

2. Einleitung

Für unser Szenario entwickeln wir drei Microservices: customer-service, product-service und order-service. Wie die Namen bereits verraten hält der customer-service Kundendaten, der product-service Produktdaten und der order-service die eigentliche Bestellungen bereit. Um die Beispielanwendung schmal zu halten existiert keine Persistenzschicht, die Daten sind statisch im Code.

API Beschreibung: 

  • order-service/orders/
  • order-service/orders/{id}
  • customer-service/customers
  • customer-service/customers/{id}
  • product-service/products
  • product-service/products/{id}

Um die Integration zwischen allen Services zu betrachten interessiert uns der Aufruf order-service/orders/{id} , hierbei erfolgen mehrere Serviceaufrufe, zum einen an den customer-service um den jeweiligen Kunden zu holen und zum anderen an product-service um die Produktinformationen zu der Bestellung zu erhalten. 

3. Implementierung: product-service und customer-service

Die beiden Microservices product-service und customer-service sind komplett identisch aufgebaut, wir schauen uns daher nur einen Service genauer an. 

Den gesamten Quellcode findet ihr auf GitHub.

https://github.com/softwarehandwerk/sample-openshift-microservices.git 

Branch: sample-communication

Die Klassen sind auf die Ordner controller, service und model aufgeteilt. In dem Ordner openshift/ sind die YAML Konfigurationsdateien für die Erstellung der OpenShift Container zu finden.

product-service
|
├── openshift
│   ├── app-deployment.yaml
│   ├── app-route.yaml
│   └── app-service.yaml
├── src
│   ├── main
│       ├── java
│           └── com
│               └── example
│                   ├── ProductServiceApplication.java
│                   ├── controller
│                   │   └── ProductController.java
│                   ├── model
│                   │   └── Product.java
│                   └── service
│                       └── ProductService.java
├── pom.xml

Im folgendem sehen wir die pragmatische Implementierung der Klassen Product, ProductService und ProductController.

@Data
@Builder
public class Product {
    private String id;
    private String name;
    private String description;
    private String price;

    private String podInfo; // 1)
}

Das Model enthält neben den Produktinformationen das Feld podInfo 1), dieses werden wir benötigen um später das Load Balancing zu demonstrieren.

@Service
public class ProductService {

    private List<Product> productList = List.of(
      Product.builder()
             .id("1")
             .name("iPhone 11 pro“)
             .description("Pro Kameras. Pro Display. Pro Performance.“)
             .price("889").build(),
     Product.builder()
             .id("2")
             .name("Xiaomi mi 9")
             .description("128GB Schwarz Dual SIM hat 6GB RAM und eine 48 MP")
             .price("309").build(),
     Product.builder()
             .id("3")
             .name("16 MacBook Pro")
             .description("Native Auflösung von 3072 x 1920 Pixeln bei 226 ppi")
             .price("2.699").build() ); // 2)

    public List<Product> getAll(){
        return productList;
    }

    public Product getById(String id){
        return productList.stream()
                .filter(product -> product.getId().equals(id))
                .findFirst().orElse(Product.builder().build());
    }
}

Der ProductService greift auf keine Persistenzschicht sondern beinhaltet statische Werte 2) mit denen der Service arbeitet.

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAll(){
        return productService.getAll();
    }

    @GetMapping("/{id}")
    public Product findById(@PathVariable("id") String id){
        Product product = productService.getById(id);
        product.setPodInfo(System.getenv("HOSTNAME")); // 3)
        return product;
    }
}

Im RestController wird beim Zurückgeben der Produktdaten /products/{id} die PodInfo 3) mit System.getenv("HOSTNAME“) gesetzt. Hierdurch identifizieren wir später welcher Container die Daten zurück liefert.

4. Implementierung: order-service mit OpenFeign

Der order-service greift auf die anderen Services zu und holt sich von ihnen die Daten. Von der Struktur ist er genauso aufgebaut wie customer-service und product-service


Der OrderService hält ebenfalls statische Testdaten bereit. Eine Order hat Informationen über Kunden und Produkte. Im folgendem Listing sehen wir, dass über customerId und productIdList die Integration hergestellt wird. Anhand dieser Beziehung demonstrieren wir, wie die Auflösung der IDs funktioniert.

private List<Order> orderList = List.of(
       Order.builder().id("1").status("STORNIERT").customerId("2").productIdList(List.of("1","2","3")).build(),
       Order.builder().id("2").status("VERSENDET").customerId("3").productIdList(List.of("2")).build(),
       Order.builder().id("3").status("GELIEFERT").customerId("1").productIdList(List.of("1","1","1","1")).build()
);

Deklarative REST Clients mit OpenFeign

Um Kunden- und Produktinformationen zu bekommen, erstellen wir mit Hilfe von OpenFeign einen Client für die REST Endpoints. Wir beschreiben hierfür ein Interface mit bekannten Spring MVC Annotation, die wir aus dem Bereitstellen von REST Endpoints bereits kennen. Den Rest übernehmen für uns Feign-spezifische Annotation. 

OpenFeign ist mittlerweile Teil des Umbrella Projektes Spring Cloud für den es auch einen eigenen Spring Starter existiert.

pom.xml

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Wir aktivieren die Verwendung von OpenFeign mit der Annotation @EnableFeignClients und die eigentlichen HTTP-Clients bekommen die Annotationen @FeignClient wo wir auch die URL der Endpoints deklarieren.

CustomerClient:

@FeignClient(name = "customers", url = "${microservices.customer.url}")
public interface CustomerClient {

    @GetMapping("/customers/")
    List<Customer> getAllCustomers();

    @GetMapping("/customers/{id}")
    Customer getCustomerById(@PathVariable("id") String id);
}

ProductClient:

@FeignClient(name = "products", url = "${microservices.product.url}")
public interface ProductClient {

    @GetMapping("/products/")
    List<Product> getAllProducts();

    @GetMapping("/products/{id}")
    Product getProductById(@PathVariable("id") String id);
}

application.properties

microservices.product.url=http://product-service:8080
microservices.customer.url=http://customer-service:8080

5. Service Discovery und Load Balancing

Die URLs für den FeignClient verweisen auf die Namen der Container, dadurch wird die Service Discovery komplett an OpenShift und Kubernetes übergeben. Dies hat ebenfalls den Vorteil, dass auch das Load Balancing von der Orchestrierungsplattform übernommen wird. Es ist nicht notwendig eigene Dienste für Service Discovery und Load Balancing aufzusetzen. 

Um das Load Balancing zu demonstrieren haben wir in die Model Entitäten die Podinformation in podInfo hinzugefügt, die im Controller kurz vor dem Zurückliefen der Objekte gesetzt werden.

Anhand der nächsten Abbildung sehen wir, dass wir in OpenShift die Pod Anzahl vom product-service auf drei Pods erhöht haben. Dies könnt ihr in der Konsole machen oder bequem über die OpenShift Web Oberfläche. Es ist weiterhin keine weitere Konfiguration in der Anwendung notwendig, kein zusätzliches registrieren der zusätzlichen Instanzen, die Auflösung erfolgt über die Namen der Services. 

Wenn wir anschließend den Aufruf vom http://order-service-myproject.192.168.64.6.nip.io/orders/2 mehrmals hintereinander ausführen, sehen wir, dass sich die PodInfo verändert und die Requests von verschiedenen Pods verarbeitet werden.

$ curl http://order-service-myproject.192.168.64.6.nip.io/orders/2

{"id":"2","status":"VERSENDET","customer":{"id":"3","firstName":"Andy","lastName":"Müller","podInfo":"customer-service-4-9knqd"},"productList":[{"name":"Xiaomi mi 9","description":"128GB Schwarz Dual SIM hat 6GB RAM und eine 48 MP","price":"309","podInfo":"product-service-8-7qbzc"}]}

Das Load Balancing erfolgt über einen HAProxy, die Default Strategie hierbei ist Round Robin und kann bei Bedarf angepasst werden.

6. Deployen der Microservices auf OpenShift

Die YAML-Konfigurationsdateien zum Deployen der Anwendung in OpenShift sind im openshift/ Ordner zu finden. Wir benutzen das Maven Plugin fabric8-maven-plugin, um ein Docker Image zu bauen und dieses anschließend in das interne OpenShift Repository zu pushen.

Wie baue ich ein Docker Image für meine Spring Boot Anwendung

Wie deploye ich meine Spring Boot Anwendung in OpenShift

Nach dem erfolgreichen Bauen eines Docker Images, wie wir es bereits in den oben erwähnten Artikeln kennengelernt haben, können wir über die YAML Konfigurationsdateien OpenShift Container erstellen:

$ oc apply -f app-deployment.yaml
$ oc apply -f app-service.yaml
$ oc apply -f app-route.yaml

7. Fazit

Es gibt unterschiedliche Ansätze um die Kommunikation und die Bereitstellung von Microservices zu realisieren. Die Anfänge kamen aus dem Open Source Netflix Stack mit Diensten wie Heureka, Hystrix, Zuul usw. und sind auch irgendwann zum Teil in das Umbrella Projekt Spring Cloud gewandert. Mit Kubernetes und OpenShift wurden die Themen wie Service Discovery, Load Balancing, API Gateways und viele weitere auf der Platformebene angegangen. In diesem Artikel haben wir uns angeschaut wie Microservices nativ auf OpenShift kommunizieren können. 

https://github.com/softwarehandwerk/sample-openshift-microservices.git (branch: sample-communication)

Source Code auf GitHub

Read More
Kommunikation von Microservices auf OpenShift – REST mit OpenFeign

1. Überblick

In diesem Artikel schauen wir uns an, wie eine Spring Boot Anwendung in einen Docker-Container verpackt und nach OpenShift deployt wird. Das Deployment werden wir lokal auf einer OpenShift (Minishift) Instanz durchführen, hierfür verwenden wir für das Bauen des Containers und das anschließende Deployen das Maven Plugin fabric8-maven-plugin.

Das Plugin unterstützt dich, deine Java Anwendung auf Kubernetes oder OpenShift zu bringen, hierfür übernimmt es folgende Aufgaben:

  • Bauen des Docker-Containers mit deiner Anwendung 
  • Erstellen von Kubernetes / OpenShift Service und Deployment Konfigurationsdateien

Wir lassen uns von dem Plugin den Container mit unserer Anwendung bauen und diesen in das OpenShift Repository laden. Um den Deploy-Vorgang transparent zu halten, erzeugen wir die notwendigen Konfigurationsdateien manuell. 

2. Minishift starten und Projekt anlegen

$ minishift start
 
Server Information ...
OpenShift server started.
 
The server is accessible via web console at:
    https://192.168.64.5:8443/console

Nach erfolgreichem Start vom Minishift wird die Webadresse in der Konsole ausgegeben. Über den OpenShift Client loggen wir uns auf der Kommandozeile ein:

$ oc login https://192.168.64.5:8443
Username: developer
Password: developer
Login successful.

Wir erstellen ein neues Projekt:

$ oc new-project myproject --display-name="My Project"

3. Einbinden des Maven Plugins 

In unsere pom.xml fügen wir das Plugin fabric8-maven-plugin hinzu:

pom.xml

<plugin>
   <groupId>io.fabric8</groupId>
   <artifactId>fabric8-maven-plugin</artifactId>
</plugin>

Das Plugin unterstützt einen Zero-Configuration Ansatz, bei dem der Entwickler nichts einzustellen braucht. Es werden Default Generatoren zum Bauen eines Docker Images verwendet. Weiterhin werden Default Ports beim Starten der Anwendung veröffentlicht. Der Ansatz ist ideal für kleine und simple Anwendung. Es ist aber auch möglich über XML-Konfiguration das Bauen des Docker Images selbst zu bestimmen, ähnlich zum Maven Plugin docker-maven-plugin, welches wir im Artikel 

Eine Spring Boot Anwendung im Docker-Container bauen

eigerichtet haben.

Für diesen Artikel reicht uns der Zero-Configuration Ansatz vollkommen aus, um unsere Beispielanwendung nach OpenShift zu deployen.

4. Bauen und Pushen des Docker Images 

Das Bauen und Pushen des Docker Images in das OpenShift Repository übernimmt für uns vollständig das Plugin fabric8-maven-plugin, wir müssen lediglich folgendes Maven Goal ausführen:

$ mvn clean package fabric8:build 

Mit diesem Befehl passiert schon die ganze Magie, das Docker Image wird gebaut und in das lokale OpenShift Repository gepusht. Da wir im ersten Schritt Minishift bereits gestartet haben und unser neu angelegtes Projekt ausgewählt haben, wird das gebaute Docker Image im Repository des neuen Projektes abgelegt.

Für das Bauen des Docker-Images wird ein eigener Source2Image Container verwendet, im /target Verzeichnis unserer Anwendung können wir uns das Dockerfile anschauen:

Dockerfile

FROM fabric8/s2i-java:2.3
ENV JAVA_APP_DIR=/deployments
LABEL org.label-schema.description="Demo project for Spring Boot" org.label-schema.version=0.0.1-SNAPSHOT org.label-schema.schema-version=1.0 org.label-schema.build-date=2020-02-24T17:48:35.576160 org.label-schema.name=sample-openshift-app org.label-schema.vcs-ref=c2d85d4d92bb521c77e912cb7641a6d2d9dda811 org.label-schema.url=https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/sample-openshift-app org.label-schema.vcs-url=https://github.com/softwarehandwerk/sample-openshift-app.git
EXPOSE 8080 8778 9779
COPY maven /deployments/

Sobald das Pushen in das lokale OpenShift erfolgt ist, können wir uns den ImageStream auch in der OpenShift Weboberfläche anschauen.


Hierfür gehen wir, in unserem Fall auf folgende URL https://192.168.64.5:8443/console, anschließend sehen wir unter Builds > Images unseren frisch gebauten ImageStream.

Image Stream in der Web Console

5. Erstellen von Deployment / Service / Route Konfigurationen 

Da wir das gebaute Docker-Image im ImageStream vom OpenShift vorliegen haben, müssen wir OpenShift nur noch mitteilen, wie er den Container starten und orchestrieren soll. Auch diesen Schritt kann das Maven Plugin fabric8-maven-plugin für uns übernehmen. Um den Vorgang transparent zu halten, erzeugen wir die Konfigurationsdateien aber manuell.

Deployment Config: app-deployment.yaml

kind: "DeploymentConfig"
apiVersion: "v1"
metadata:
  name: "sample-openshift-app"
spec:
  template:
    metadata:
      labels:
        name: "sample-openshift-app"
    spec:
      containers:
        - name: "sample-openshift-app"
          ports:
            - containerPort: 8080
              protocol: "TCP"
  replicas: 1 
  triggers: 
    - type: "ConfigChange"
    - type: "ImageChange"
      imageChangeParams:
        automatic: true
        containerNames:
          - "sample-openshift-app"
        from:
          kind: "ImageStreamTag"
          name: "sample-openshift-app:latest"
  strategy: 
    type: "Rolling"
  paused: false
  revisionHistoryLimit: 2

Service Config: app-service.yaml

apiVersion: "v1"
kind: "Service"
metadata:
  name: "sample-openshift-app-service"
spec:
  ports:
    - name: "web"
      port: 8080
      targetPort: 8080
  selector:
    name: "sample-openshift-app"

Route Config: app-route.yaml

apiVersion: "v1"
kind: "Route"
metadata:
  name: "sample-openshift-app-route"
spec:
  path: "/"
  to:
    kind: Service
    name: "sample-openshift-app-service"

Für die Konfigurationsdateien erstellen wir in unserer Anwendung einen Ordner openshift/ und legen dort die oben beschriebenen YAML-Konfigurationen ab. Anschließend laden wir nacheinander die Konfigurationen über die Kommandozeile in OpenShift:

oc apply -f app-deployment.yaml
oc apply -f app-service.yaml
oc apply -f app-route.yaml

Die Orchestrierung deiner Anwendung übernimmt nun wie definiert OpenShift, in der Webkonsole kannst du deinen gestarteten Pod sehen und über die bereitgestellte Route im Browser aufrufen: http://sample-openshift-app-route-myproject.192.168.64.5.nip.io/

6. Fazit

Mit Hilfe des Maven Plugins von fabric8 ist das Deployen einer Java Anwendung auf OpenShift ziemlich einfach. Die Erstellung eines Docker-Containers und das eigentliche Deployen kann in den Maven Lifecycle integriert werden. Dadurch gelangt jede Code-Änderung automatisiert auf die Zielplatform OpenShift oder Kubernetes.

https://github.com/softwarehandwerk/sample-openshift-app

Source Code auf GitHub
Read More
Eine Spring Boot Anwendung im Docker-Container nach OpenShift deployen

1. Überblick

In diesem Artikel schauen wir uns an wie wir mit dem Maven Plugin docker-maven-plugin unsere Spring Boot Anwendung in den Docker Container verpacken und diesen dann ausführen. Mit dem Maven Plugin von fabric8 ist es ganz einfach aus der Spring Boot Anwendung ein Docker Image zu bauen, testen und über eine Continuous Delivery Pipeline zu deployen.

2. Maven Dependency

Als erstes fügen wir in unserer pom.xml das Maven Plugin docker-maven-plugin hinzu:

<plugin>
   <groupId>io.fabric8</groupId>
   <artifactId>docker-maven-plugin</artifactId>
   <configuration>
      <images>
         <image>
            <name>sample/dockerjava</name>
            <build>
               <contextDir>${project.basedir}</contextDir>
               <assembly>
                  <descriptorRef>artifact</descriptorRef>
               </assembly>
            </build>
         </image>
      </images>
   </configuration>
</plugin>


Wir konfigurieren das Plugin mit einer externen Dockerfile um den Container zu beschreiben. Dafür definieren wir im Element contextDir den Pfad zu der externen Dockerfile. In unserem Fall liegt die Dockerfile direkt neben der pom.xml, hierfür setzen wir  den contextDir auf ${project.basedir}. Über das Element assembly wird gesteuert, was gebaut werden soll und welche Dateien hinzugefügt sollen.


Vordefinierte Assembly Deskriptoren

  • artifact-with-dependencies fügt das Artefakt mit allen Dependencies hinzu.
  • artifact fügt das Artefakt ohne Dependencies hinzu. 
  • project fügt das ganze Maven Projekt ohne /target Verzeichnis hinzu.
  • rootWar fügt das Artefakt als ROOT.war hinzu.

Das Beispiel zeigt ausschließlich wie ein Docker Image gebaut wird, es ist aber auch möglich ein automatisches Ausführen des Containers im configuration Element zu konfigurieren um es zum Beispiel in Integration Tests einzubinden.   

3. Dockerfile

Im nächsten Schritt erstellen wir die Dockerfile und legen diese neben der pom.xml ab:

FROM openjdk:8-jre-alpine
COPY maven/*.jar /app.jar
EXPOSE 8080
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", "/app.jar"]

Für den Docker Containern verwenden wir das openjdk:8-jre-alpine Image, es enthält alles was wir benötigen um Java Anwendungen im Docker Container auszuführen. Unser Artefakt der Spring Boot Anwendung wird in den maven/ Ordner des Docker Containers hinzugefügt. Wir kopieren die Anwendung auf die Root Ebene und veranlassen in der letzten Zeile das Starten der Anwendung. Damit wir auch Zugriff von Außen auf die Anwendung haben, muss der Port 8080 exposed werden.  

4. Docker Container ausführen

Mit mvn docker:build wird der Container gebaut, hierbei wird in diesem Schritt unter target/docker/sample/dockerjava ein build/ Ordner generiert wo zum einen das Dockerfile zu finden ist und zum anderen das Artefakt der Spring Boot Anwendung. In dem tmp/ Ordner liegt das gebaute Docker Image mit unserer Spring Boot Anwendung.

Nach dem Bauen findet ihr über den Befehl docker images das fertige Image im Repository, anschließend könnt ihr es im Docker starten:

docker run -p 8080:8080 sample/dockerjava 

5. Fazit

Mit dem Maven Plugin von fabric8 lässt sich aus einer Spring Boot Anwendung ganz einfach ein Docker Container bauen, testen und ausführen. In diesem Artikel haben wir uns auf das Bauen konzentriert.  Wie ihr den gebauten Docker Container auf OpenShift deployen könnt, erfahrt ihr im nächsten Artikel.

> Eine Spring Boot Anwendung im Docker-Container nach OpenShift deployen

Source Code auf GitHub: https://github.com/softwarehandwerk/sample-docker-java-app

Read More
Eine Spring Boot Anwendung im Docker-Container bauen