Skip to main content

GitLab CI Pipeline - Containerize and Deploy a Javen Maven Application as Fat Jar Package, Publish Unit Test Results via GitLab Pages

1453 words·
GitLab GitLab CI CI Pipeline GitLab Pages Java Maven Fat Jar Maven Surefire Report Plugin Multistage Dockerfile Unprivileged Container
Table of Contents
GitHub Repository Available

Overview
#

My GitLab Setup
#

In this tutorial I’m using the following setup based on Ubuntu 22.04 servers, GitLab is containerized:

192.168.70.4 # GitLab Server
192.168.70.6 # Deployment Server, with Docker platform installed

Simple Javen Maven Project
#

  • The HelloMaven-app Java Maven application is a basic HelloWorld example that utilizes the Joda-Time library to display the current date and time.

  • To ensure the container remains running after the application completes, the tail -f /dev/null command has been added to the container’s ENTRYPOINT:

# Container entrypoint
ENTRYPOINT ["sh", "-c", "java -jar app.jar && tail -f /dev/null"]
  • The functionality of the application can be verified by inspecting the container logs:
# Verify the Javen Maven application
docker logs c505a764bce1

# Shell output:
Hello, from Maven. Time:
2024-12-02 16:18:29
  • More details about this Javen Maven project can be found in my previous blog post “Java Programming Language”.



GitLab Repository
#

File and Folder Structure
#

GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml
├── HelloMaven-app  # Simple Java Maven Application
│   ├── .mvn
│   │   ├── jvm.config
│   │   └── maven.config
│   ├── pom.xml  # Maven configuration file
│   └── src
│       ├── main
│       │   └── java
│       │       └── com
│       │           └── mycompany
│       │               └── app
│       │                   └── App.java  # Main Java file
│       └── test
│           └── java
│               └── com
│                   └── mycompany
│                       └── app
│                           └── AppTest.java  # Test file
└── README.md

GitLab CI Pipeline
#

  • .gitlab-ci.yml
### Variables
variables:
  DEPLOY_IP: "192.168.70.6"  # Deployment server IP
  DEPLOY_USER: "gitlab-deployment"  # Deployment server SSH user
  DEPLOY_PORT_HOST: 8080  # Host port
  DEPLOY_PORT_CONT: 8080  # Container port
  CONTAINER_NAME: "simple-javen-maven-project"  # Name of the deployed container
  # Set a custom Maven local repository path to avoid polluting the default location and enable caching
  MAVEN_OPTS: "-Dmaven.repo.local=/builds/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/.m2/repository"


### Stages
stages:
  - test
  - pages
  - build
  - deploy


### Test
test:
  image: maven:3.9.4-eclipse-temurin-17
  stage: test
  script:
    # Navigate to the HelloMaven-app directory
    - cd HelloMaven-app
    # Output a message to indicate the start of test execution
    - echo "Running Maven tests..."
    # Run the tests using Maven
    - mvn test
    # Generate an HTML test report using the Maven Surefire Report plugin
    - mvn surefire-report:report
  cache:
    paths:
      # Cache the Maven dependencies to speed up subsequent builds
      - .m2/repository
  artifacts:
    when: always  # Always upload artifacts, regardless of whether the job succeeds or fails
    paths:
      # Collect XML test results
      - HelloMaven-app/target/surefire-reports/*.xml
      # Collect the HTML test report
      - HelloMaven-app/target/site/surefire-report.html
    expire_in: 1 week


### Deploy Test Results to GitLab Pages
pages:
  image: maven:3.9.4-eclipse-temurin-17
  stage: pages
  needs:
    # Run this job only if the 'test' job succeeds
    - test
  script:
    # Ensure the public directory exists
    - mkdir -p public
    # Move the report to the 'public/' directory for GitLab Pages
    - mv HelloMaven-app/target/site/surefire-report.html public/index.html
    # Debugging: List the public directory contents
    - ls -l public
  artifacts:
    paths:
      # Specify the 'public/' directory to be published via GitLab Pages
      - public/


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the Dockerfile in the current directory
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
    # Push the built Docker image to the GitLab Container Registry
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  rules: 
    # Rule: Run this job only for main branch and if the Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile


### Deploy Container to Virtual Machine
deploy_container:
  stage: deploy
  image: alpine:latest
  needs:
    # Run this job only if the 'build_image' job succeeds
    - build_image
  before_script:
    # Update the package index, install the OpenSSH client for SSH connections
    - apk update && apk add openssh-client
    # If the private SSH key file ($ID_RSA) exists, set secure permissions (read/write for the owner only)
    - if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
  script:
    #  SSH into the deployment server, log in to the GitLab Container registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
     # SSH into the deployment server, pull the image from the registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
    # SSH into the deployment server, remove the existing container (if it exists)
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker container rm -f $CONTAINER_NAME || true"
    # SSH into the deployment server, run the new container
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker run -d -p $DEPLOY_PORT_HOST:$DEPLOY_PORT_CONT --restart=unless-stopped --name $CONTAINER_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  rules:
    # Rule: Run this job only for main branch
    - if: $CI_COMMIT_BRANCH == "main"

Dockerfile
#

### Stage 1: Build Stage

# Use a lightweight JRE image as the base image / define as build stage
FROM maven:3.9.4-eclipse-temurin-17 AS build

# Set the working directory inside the build stage
WORKDIR /app

# Copy the Maven project files into the container
COPY HelloMaven-app/pom.xml ./
COPY HelloMaven-app/src ./src

# Build the application with dependencies included
RUN mvn clean compile assembly:single



### Stage 2: Runtime Stage

# Use a lightweight JRE image as the base image
FROM eclipse-temurin:17-jre-alpine

# Create a user & group
RUN addgroup -S app-group && adduser -S app-user -G app-group

# Set the working directory inside the runtime stage
WORKDIR /app

# Copy the built JAR file from the build stage to the runtime stage
COPY --from=build /app/target/HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar app.jar

# Change ownership of the application files
RUN chown -R app-user:app-group /app

# Switch to the non-root user
USER app-user

# Expose port 80 for the web server
EXPOSE 80

# Command to run the application
ENTRYPOINT ["sh", "-c", "java -jar app.jar && tail -f /dev/null"]



Java Maven Application
#

Dependencies & Plugins: pom.xml
#

  • HelloMaven-app/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mycompany.app</groupId>
  <artifactId>HelloMaven-app</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>HelloMaven-app</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>5.11.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Optionally: parameterized tests support -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-params</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Joda-Time dependency -->
    <dependency>
      <groupId>joda-time</groupId>
      <artifactId>joda-time</artifactId>
      <version>2.10.14</version>
    </dependency>
    <!-- Junit dependency -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.3.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.13.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.3.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.4.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.12.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>

        <!-- Add maven-assembly-plugin -->
        <plugin>
          <artifactId>maven-assembly-plugin</artifactId>
          <configuration>
            <archive>
              <manifest>
                <mainClass>com.mycompany.app.App</mainClass>
              </manifest>
            </archive>
            <descriptorRefs>
              <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
          </configuration>
        </plugin>

        <!-- Add maven-surefire-report-plugin -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-report-plugin</artifactId>
            <version>3.1.0</version>
        </plugin>

      </plugins>
    </pluginManagement>
  </build>
</project>

Java Application: App.java
#

  • HelloMaven-app/src/main/java/com/mycompany/app/App.java
// Define the package name for this class: A package is a namespace that organizes related classes and interfaces
package com.mycompany.app;

// Import the DateTime class from the Joda-Time library
import org.joda.time.DateTime;

// Define a public class named "App"
public class App {
    // Print some text
    public static void main(String[] args) {
        System.out.println("Hello, from Maven. Time:");

        // Use Joda-Time to get and print the current date and time
        DateTime currentTime = new DateTime();
        System.out.println(currentTime.toString("yyyy-MM-dd HH:mm:ss"));
    }
}

Unit Test: AppTest.java
#

  • HelloMaven-app/src/test/java/com/mycompany/app/AppTest.java
package com.mycompany.app;

import static org.junit.Assert.assertTrue;

import org.joda.time.DateTime;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public class AppTest {

    @Test
    public void testMainOutput() {
        // Capture the console output
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));

        // Run the main method
        App.main(new String[]{});

        // Verify the output contains the greeting
        String output = outContent.toString();
        assertTrue("Output should contain the greeting", output.contains("Hello, from Maven. Time:"));

        // Verify the output contains a correctly formatted timestamp
        DateTime now = new DateTime();
        String expectedDate = now.toString("yyyy-MM-dd");
        assertTrue("Output should contain the current date", output.contains(expectedDate));

        // Reset the console output
        System.setOut(System.out);
    }
}



Verify the Deployment
#

Verify the Container
#

# List Docker containers
docker ps

# Shell output:
CONTAINER ID   IMAGE                                                            COMMAND                  CREATED          STATUS          PORTS                                               NAMES
8ed8d440f97b   gitlab-registry.jklug.work/java/simple-java-maven-project:main   "sh -c 'java -jar ap…"   32 seconds ago   Up 31 seconds   80/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   simple-javen-maven-project

Verify Javen Maven Application
#

# Verify the Javen Maven application
docker logs c505a764bce1

# Shell output:
Hello, from Maven. Time:
2024-12-02 16:18:29

Verify Test Stage Artifacts
#

  • Go to: (Project) “Build” > “Artifacts”

  • Download the artifacts.zip file from the “test” job

The artifacts.zip file includes both the HTML report surefire-report.html and the XML file TEST-com.mycompany.app.AppTest.xml:

artifacts.zip
└── HelloMaven-app
    └── target
        ├── site
        │   └── surefire-report.html
        └── surefire-reports
            └── TEST-com.mycompany.app.AppTest.xml



Links #

jueklu/gitlab-ci-simple-java-maven-project

GitLab CI pipeline that containerizes and deploys a Java Maven application. Integration test results are published via GitLab Pages.

Java
0
0