Gitlab CI/CD+Runner+Docker implements automatic packaging and deployment of springboot projects

Article directory

  • Preface
  • 1. Gitlab Runner deployment
    • 1. Obtain Runner registration token
    • 2. Register Runner
    • 3. Configure Runner
  • 2. Configure GitLab CI
  • 3. Run the pipeline
  • 4. Use scheduled scripts for backup & deployment
  • Reference URL
  • Summarize

Foreword

The software and versions used in this article are as follows:
Gitlab: 14.6.1; Gitlab-Runner: 16.3.1

This article uses Gitlab CI/CD + Gitlab Runner to realize the function of automatically packaging and deploying springboot projects.
Using automatic CI/CD can reduce the burden of maintenance and avoid problems such as human operation errors.

  • Both Gitlab and Gitlab-Runner use Docker for deployment, and Runner also uses Docker as the executor.
  • In this article, Runner is only responsible for packaging and outputting to the specified directory. The springboot project is backed up & deployed by a scheduled script.
  • This article contains the author’s CI configuration, backup & deployment timing scripts
  • Regarding the deployment of Gitlab, this article will not go into details.

1. Gitlab Runner deployment

1. Obtain Runner registration token

Use Gitlab’s administrator account (default is root) to open the Gitlab-Runner management interface in the sequence shown below
Gitlab-Runner registration interface
Copy the Runner’s Registration Token, which will be used when deploying the Runner later.
Runner Registration Token

2. Register Runner

Use the following command to pull the Runner image. Note: This article does not use the official image

docker pull bitnami/gitlab-runner

Create and run the Runner using the following commands
When the Runner is registered, config.toml will be generated to /etc/gitlab-runner/config.toml, but when it is actually run, /home/gitlab-runner/.gitlab-runner/config.toml is used, so it is mapped to the same file. Easy to modify
-d runs in the background, -names it as [Gitlab_Runner], --restart always starts automatically, -v mounts the host directory
[Host directory] is modified according to the actual situation. The docker.sock path generally does not need to be modified

docker run -d --name Gitlab_Runner --restart always \
-v [host directory]/.gitlab-runner/config.toml:/etc/gitlab-runner/config.toml \
-v [host directory]:/home/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
bitnami/gitlab-runner:latest

Next, enter the container as root to perform operations ([Gitlab_Runner] needs to be replaced with the actual container name)

docker exec -it -u root Gitlab_Runner bash

As shown in the figure below, enter the command to start registering the Runner:
Because the author's Gitlab and Runner are under the same Docker network, the Gitlab address here is the intranet address. Please modify it according to the actual situation
Three configurations that can be empty and can be modified in Gitlab at any time

gitlab-runner register

Runner registration

3. Configure Runner

After the Runner is registered, check the corresponding files in the host as follows:
Runner initial configuration
docker in docker mounting path needs to be modified according to the actual situation, the syntax is consistent with the docker instruction: host path: path within the container
Please note that the former path here is the path of the host, not the path of the Runner container

Under [runners.docker], add: pull_policy = “if-not-present” to enable the Runner to pull only when the image does not exist, as follows:

pull_policy = "if-not-present" #Default is always, optional: always, if-not-present, never

It is recommended to map maven and its warehouse path so that the local warehouse can be reused. The configuration is as follows:

volumes = ["/cache", "[maven path]:/root/.m2", "[jar package storage path]:/app/build"]
# Note: The maven warehouse path is: [maven path]/repository

The author has configured multiple caches, as follows:

[runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

After restarting the Runner container, you can see the newly registered Runner in Gitlab
Newly registered Runner

At this point, Gitlab Runner has been deployed and configured.

2. Configure GitLab CI

As shown below, enter the CI configuration file editing interface, and then modify the preset CI configuration file:
CI Editor

Gitlab has provided a large number of templates for reference, including maven; there are also detailed syntax instructions in the official documentation. Therefore, this article will not go into details, but only introduces the author's CI configuration ideas
Due to actual needs, the author did not use Runner to encapsulate the project into a Docker image and run it, but only output the jar package. Actions such as backup and deployment are performed by scheduled tasks.
So the CI configuration only executes mvn package and moves the jar package to the specified directory, so only the build phase is needed
The author’s CI configuration example is as follows:

# Script variables
variables:
  # Project port number
  PORT_NUM: "[port number]"
  # Information printed during the build phase
  BUILD_PREFIX_INFO: "$CI_COMMIT_BRANCH build started\
Build creator: $GITLAB_USER_NAME\
Branch committer: $CI_COMMIT_AUTHOR\
Packaging start time: $CI_PIPELINE_CREATED_AT"
  BUILD_SUFFIX_INFO: "$CI_COMMIT_BRANCH build completed"
  # Project file root directory
  # "[jar package storage path]:/app/build"] in the Runner container specifies that /app/build is mapped to the host machine
  BASE_PATH: "/app/build/$CI_PROJECT_NAMESPACE/$CI_PROJECT_TITLE"
  # New package path
  PACKAGE_PATH: "$BASE_PATH/new"
  # New package name: project name-port number.jar
  PACKAGE_NAME: "$CI_PROJECT_TITLE-$PORT_NUM.jar"
  # Backup path
  BACKUP_PACKAGE_PATH: "$BASE_PATH/backup"
  # maven packaging parameters
  MAVEN_PACKAGE_OPTS: "-Djansi.passthrough=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8
  -DskipTests=true -Dmaven.test.skip=true"
# During the construction phase, package the project and place it in the corresponding path, and the server will automatically deploy the script for backup and deployment.
# Jar package naming rules: project name-port number.jar
#Final product file structure:
# / Project file root directory, that is, /app/build/
# ╚═══╦══Group or username/
# ╚═══╦══Project name/
# ╠═══╦══new/
# ║ ╚══════new.jar output jar package
# ╠═══╦══backup/
# ║ ╚══════backup.jar
# ╚══════old.jar
build: # stage name, can be modified arbitrarily
  stage: build
  # Only the master branch is executed
  only:
    - master
  before_script:
    - echo -e "$BUILD_PREFIX_INFO"
  script:
    - mvn $MAVEN_PACKAGE_OPTS clean package
    - if [ ! -d $PACKAGE_PATH ];
        then echo -e "Create a new package path for the project"; mkdir -p $PACKAGE_PATH;
      fi
    - if [ ! -d $BACKUP_PACKAGE_PATH ];
        then echo -e "Create project backup path"; mkdir -p $BACKUP_PACKAGE_PATH;
      fi
    - if [ -f target/*.jar ];
        then cp target/*.jar $PACKAGE_PATH/$PACKAGE_NAME; # Output the jar package to [jar package storage path]
        else echo -e "New project package not found, migration will not be performed"; exit 1;
      fi
  # If you do not need to output the jar package to the Gitlab pipeline product, you do not need the following steps
    - cp target/*.jar $CI_PROJECT_DIR/$PACKAGE_NAME # Output to the Gitlab artifact directory
  # Output the packaged jar package to Gitlab pipeline product
  artifacts:
      name: $CI_PROJECT_NAMESPACE-$CI_PROJECT_TITLE-$CI_COMMIT_BRANCH
      paths:
        - $CI_PROJECT_DIR/$PACKAGE_NAME
      expire_in: 1 week
  after_script:
    - echo -e "$BUILD_SUFFIX_INFO"

3. Run the pipeline

When the .gitlab-ci.yml file exists in the git root directory of the project, if the relevant branches are submitted or merged, Gitlab will automatically create and execute the pipeline and related jobs.
The picture below shows a successful job. The generated jar package is also found in the [jar package storage path] folder of the host machine:

4. Use scheduled scripts for backup & deployment

An example of the file structure of the author’s server is as follows:

Therefore, the timing script needs to have the following functions:






































Iterate through all items







Project Y

























Continue to traverse other items in the group







Continue to traverse other groups










Iterate through all groups









GroupX









Out of service









old.jar migrated to backup









Migrate new.jar to the project Y root directory









Start service







The author’s scheduled backup & deployment script is as follows:

BaseDir="[parent directory of group folder]"
BackupDirName="backup"
NewPackageDirName="new"
LogDirName="[Log]" # Log folder name, skip when traversing
RunScriptName="[start and stop script].sh"
StopCommand="stop" # Stop service command
StartCommand=" start" # Start service command

cd $BaseDir
echo "************Start deploying************"
if (ls -A $BaseDir/ | grep -q ".")
then
    for groupName in $(ls -A $BaseDir/)
    do
        echo -----Start checking group:$groupName-----
        for projectName in $(ls -A $BaseDir/$groupName)
        do
            thisBaseDir=$BaseDir/$groupName/$projectName
            projectPath=$groupName/$projectName
            if [ $projectName == $LogDirName ]
                then continue
            fi
            if [ ! -d $thisBaseDir/$BackupDirName ]
                then echo $projectPath Backup folder not created; mkdir -p $thisBaseDir/$BackupDirName;
            fi
            if [ ! -d $thisBaseDir/$NewPackageDirName ]
                then echo $projectPath did not create new package folder; mkdir -p $thisBaseDir/$NewPackageDirName;
            fi
            if [ -f $thisBaseDir/$NewPackageDirName/* ]
                then
                    New files have been found in echo $projectPath and deployment begins;
                    cd $thisBaseDir;
                    if [ -f $thisBaseDir/$RunScriptName ]
                        then $thisBaseDir/$RunScriptName$StopCommand;
                        else echo $projectPath not found stop script! ;
                    fi
                    if [ -f $thisBaseDir/*.jar ]
                        then
                            for file in $thisBaseDir/*.jar
                            do
                                # Backup file name format: old_YYYYmmdd-HHMM.jar
                                mv $file $thisBaseDir/$BackupDirName/$(basename ${<!-- -->file%.*})_$(date + %Y%m%d-%H%M).jar;
                            done
                            echo "Old package backup completed";
                        else echo $projectPath did not find the old package, no backup required;
                    fi
                    mv $thisBaseDir/$NewPackageDirName/*.jar $thisBaseDir/; #The situation that the new package does not exist has been ruled out
                    echo "New package migration completed";
                    if [ -f $thisBaseDir/$RunScriptName ]
                        then $thisBaseDir/$RunScriptName$StartCommand;
                        else echo $projectPath startup script not found! ;
                    fi
                    echo $projectPath deployed;
                else echo No new file found in $projectPath;
            fi
        done
        echo -----End checking group:$groupName-----
    done
else
    echo "No new files found"
fi
echo "************Deployment completed************"
exit 0

The author’s java service start and stop script is as follows:

#!/bin/bash

# jar package file name
PROJECT_NAME="[jar package file name].jar"
# jdk path, the author did not install the java environment, but directly used the java in the jdk folder
JavaBaseDir="[jdk path]/jdk/bin"
 
function check() {<!-- -->
    PID=$(ps aux | grep $1 | grep -v grep | awk '{print $2}')
    if [[ "${PID[@]}" != "" ]];then
        echo "Service already exists: $1"
        exit 1
    fi
}
 
function check_start() {<!-- -->
    PID=$(ps aux | grep $1 | grep -v grep | awk '{print $2}')
    if [[ "${PID[@]}" != "" ]];then
        echo "Service started successfully: $1"
    else
        echo "Service startup failed: $1"
    fi
}
 
function stop_service() {<!-- -->
    PID=$(ps aux | grep $1 | grep -v grep | awk '{print $2}')
    if [[ "${PID[@]}" != "" ]];then
        ps -ef | grep $1 | grep -v grep | cut -c 9-15 | xargs kill -9
        test $? -eq 0 & amp; & amp; echo "Service shutdown successful: $1"
    else
        echo "Corresponding service not found: $1"
    fi
}
 
function start_service() {<!-- -->
    check ${<!-- -->PROJECT_NAME}
    # Run the service. If the java environment has been configured, you can use the java command directly.
    nohup $JavaBaseDir/java -Xms1000m -Xmx1000m -XX:-UseGCOverheadLimit -Dfile.encoding=utf-8 -jar ${<!-- -->PROJECT_NAME} >/dev/null 2> & amp;1 & amp;
    check_start ${<!-- -->PROJECT_NAME}
}
 
 
case $1 in
    start)
        start_service
        ;;
    stop)
        stop_service ${<!-- -->PROJECT_NAME}
        ;;
    restart)
        stop_service ${<!-- -->PROJECT_NAME}
        sleep 1
        start_service
        ;;
    *)
        echo "Usage: bash $0 start | stop | restart"
        ;;
esac

The output of one of my timing scripts is as follows:
Timed script output results

Reference URL

Official documentation
Gitlab CICD environment variables
GitLab CI/CD + GitLab Runner in Docker fully automatic deployment server web page
Automated build: gitlab gitlab-run, maven cache and gitea drone drone-run

Summary

At this point, Gitlab’s function of automatically deploying springboot projects has been implemented. Please choose what kind of Runner executor, CI/CD content, etc. to use based on your actual needs.
Thank you for reading.