2SOMEone | Jenkins持续集成与持续部署

by CUNOE, April 8, 2024

今天记录一下泡泡树洞后端项目-2SOMEone的Jenkins持续集成与持续部署是如何实现的。

架构

泡泡树洞后端项目-2SOMEone的Jenkins持续集成与持续部署采用Jenkins + Docker + Gitlab架构。

  • Jenkins:持续集成与持续部署
  • Docker:容器化
  • Gitlab:代码仓库

实现

Jenkins

Jenkins是一个开源的自动化服务器,用于自动化各种任务,包括构建、测试和部署软件。

在这里使用Jenkins进行2SOMEone的持续集成与持续部署。

安装Jenkins

# docker-compose.yml
services:
  jenkins:
    image: jenkins/jenkins:2.374
    restart: unless-stopped
    privileged: true
    user: root
    ports:
    - 8080:8080
    volumes:
    - ./jenkins-data/jenkins_home:/var/jenkins_home
    - /var/run/docker.sock:/var/run/docker.sock
    - /usr/bin/docker:/usr/bin/docker

Gitlab

Gitlab是一个基于Git的管理工具,提供了代码仓库、问题跟踪、CI/CD等功能。

在这里使用Gitlab作为2SOMEone的代码仓库。

安装Gitlab

# docker-compose.yml
services:
  gitlab:
    image: 'gitlab/gitlab-ee:14.6.4-ee.0'
    restart: always
    hostname: 'gitlab.example.com'
    ports:
      - '443:443'
      - '80:80'
      - '1022:22'
    volumes:
      - './config:/etc/gitlab'
      - './logs:/var/log/gitlab'
      - './data:/var/opt/gitlab'
    environment:
      - TZ=Asia/Shanghai

配置

为了实现代码从Gitlab和Jenkins自动触发的CI/CD流程,需要在Gitlab和Jenkins中进行配置。

我们采用的是在Gitlab中配置Webhook,当代码提交时触发Jenkins的构建任务。

Jenkins/manage/configure中配置Gitlab相关配置,添加GitlabHostCredentials以帮助Jenkins与Gitlab进行交互。

在新建的构建任务中,勾选触发器Build when a change is pushed to GitLab. GitLab webhook URL: https://jenkins/project/task, 并在Gitlab中配置Webhook,当代码提交时触发Jenkins的构建任务。

在Gitlab中打开构建项目的代码仓库,点击Settings -> Webhooks -> URL填写https://jenkins/project/task, 并勾选Push events,点击Add Webhook即可。

至于其它触发事件及其使用方法在这里不再赘述,有需要的参考官方文档。

Jenkinsfile

一般来说,我们会在代码仓库中添加Jenkinsfile文件,用于定义Jenkins的流水线任务。

这样的好处是,我们可以将流水线任务的定义与代码放在一起,利用Git的版本控制功能,方便管理和追踪。

Jenkinsfile文件的内容可以是Groovy脚本,用于定义流水线任务的各个阶段,如构建、测试、部署等。

下面是我们2SOMEone项目的Jenkinsfile文件的一个示例:

2SOMEone全流程都采用了Docker容器化,所以在流水线任务中会有很多Docker相关的操作。

通过Docker容器化,我们可以将构建、测试、部署等操作封装到Docker镜像中,方便管理和迁移,Jenkins只需要在流水线任务中调用Docker镜像即可,这样的好处是,我们可以在任何支持Docker的环境中运行流水线任务,而不用担心环境的问题。

我们将Docker镜像的构建、推送、部署等操作都放在了Jenkins的流水线任务中,这样可以实现一键式的CI/CD流程。

pipeline {
    agent any
    options {
        timestamps()
        timeout(time:1, unit:'HOURS')
        gitLabConnection gitLabConnection: 'gitlab', jobCredentialId: 'gitlab api token', useAlternativeCredential: true
    }
    post {
        success {
            updateGitlabCommitStatus name: 'jenkins', state: 'success'
            build job: 'send-status-to-feishu', parameters: [string(name: 'STATUS', value: 'SUCCESS')]
        }
        unsuccessful {
            updateGitlabCommitStatus name: 'jenkins', state: 'failed'
            build job: 'send-status-to-feishu', parameters: [string(name: 'STATUS', value: 'FAILED')]
        }
    }
    environment {
        VERSION = "v1"
        DOCKERHUB = "registry.cn-guangzhou.aliyuncs.com/leaperone/2someone"
        BUILD_DATE = sh(returnStdout: true, script: 'date -u +"%Y%m%d"').trim()
        JOB_TAG = "${JOB_NAME}-${BUILD_NUMBER}"
    }
    stages {
        stage("updateGitlabCommitStatus") {
            steps {
                updateGitlabCommitStatus name: 'jenkins', state: 'running'
            }
        }
        stage("Login DockerHub") {
            steps {
                withCredentials([usernamePassword(credentialsId: 'aliyun-docker', passwordVariable: 'ALIYUN_DOCKER_PASSWORD', usernameVariable: 'ALIYUN_DOCKER_USERNAME')]) {
                    sh 'docker login --username=${ALIYUN_DOCKER_USERNAME} --password=${ALIYUN_DOCKER_PASSWORD} registry.cn-guangzhou.aliyuncs.com'
                }
            }
        }
        stage("Build Docker Image"){
            steps{
                sh 'docker-compose -f ./docker/docker-compose-build.yml build --no-cache'
            }
        }
        stage("Push Docker Images"){
           when{
               anyOf {
                   expression {return env.BRANCH_NAME == "main"}
                   expression {return env.BRANCH_NAME == "dev"}
               }
           }
            steps{
                sh 'docker tag ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-latest-${JOB_TAG} ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-latest'
                sh 'docker push ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-latest'
                // ...
            }
        }
        stage("Archive Docker Images"){
            when{
                anyOf {
                    expression {return env.BRANCH_NAME == "main"}
                    expression {return env.BRANCH_NAME == "dev"}
                }
            }
            steps{
                sh 'docker tag ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-latest ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-${BUILD_DATE}'
                sh 'docker push ${DOCKERHUB}:bubblebox-api-${VERSION}-${BRANCH_NAME}-${BUILD_DATE}'
                // ...
            }
        }
        stage("CD in main"){
            when {
                expression {return env.BRANCH_NAME == "main"}
            }
            steps{
                sh 'ssh -i /var/jenkins_home/id_rsa_sshTo -p 220 leaperone@192.168.0.203 "bash /path/to/deploy.sh"'
                build job: 'send-status-to-feishu', parameters: [string(name: 'STATUS', value: 'DEPLOY MAIN SUCCESS')]
            }
        }
        stage("CD in dev"){
            when {
                expression {return env.BRANCH_NAME == "dev"}
            }
            steps{
                sh 'ssh -i /var/jenkins_home/id_rsa_sshTo leaperone@192.168.0.203 "bash /path/to/deploy_dev.sh"'
                build job: 'send-status-to-feishu', parameters: [string(name: 'STATUS', value: 'DEPLOY DEV SUCCESS')]
            }
        }
    }
}

具体的流水线任务定义可以根据项目的实际情况进行调整。

Jenkinsfile的编写需要一定的Groovy基础,如果不熟悉Groovy语法,可以参考官方文档或者参考一些示例。

总结

Jenkins的引入,使得2SOMEone的持续集成与持续部署变得更加简单高效,也正是因为Jenkins的加入,泡泡树洞才得以实现了如此高速的迭代与更新。

我们也考虑过GitlabCI、Github Actions等持续集成工具,但最终还是选择了Jenkins,因为Jenkins的插件生态丰富,可以满足我们的需求,同时Jenkins的流水线任务定义更加灵活,可以根据项目的实际情况进行调整。

也因为,Github Actions等工具在国内的访问速度不够理想(以及可能的收费),GitlabCI则是定制化需求难以满足,而Jenkins可以部署在自己的服务器上,访问速度更快,更加稳定,预算成本也更低,学习成本也更低。

通过Jenkins的持续集成与持续部署,我们可以实现代码提交后自动构建、测试、部署等操作,提高开发效率,减少人为错误,保证代码质量。

未来,我们还会继续优化2SOMEone的持续集成与持续部署流程,提高自动化程度,提高开发效率。

就目前而言,Jenkins已经能够满足我们的需求,我们会继续使用Jenkins进行2SOMEone的持续集成与持续部署。