Learning CI/CD with Jenkins

据粗略感受, jenkins是一个比gitlab功能更强大、可自定义化更高的持续集成工具 它可以简单的通过webhook被触发(本质上是对它的一个网址进行POST),然后通过pipeline来执行一系列的操作,可以说它允许安装的插件有多少,可执行的操作就有多少。

测试配置

由于手上暂时没有公网ip,所以gitlab,jenkins,服务器都跑在本地

部署gitlab

在你打算存储gitlab配置文件的路径下,执行

docker run -itd -p 9980:80 -p 9922:22 -v ./etc:/etc/gitlab -v ./log:/var/log/gitlab -v ./opt:/var/opt/gitlab --restart always --privileged=true --name gitlab gitlab/gitlab-ce

运行docker exec -it gitlab bash进入终端 编辑配置文件/# vi /etc/gitlab/gitlab.rb 在文件开头稍微往下的地方找到external_url的注释 在其下方添加

external_url 'http://192.168.8.246:9980'                                                           
nginx['listen_port'] = 80

不然的话,可能无法正常git仓库操作(因为会将ip识别为容器ip) 在保存后重新载入配置gitlab-ctl reconfigure

启动后访问http://localhost:9980,管理员用户名是root 初始密码通过docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password获得 在管理员面板找到'网络',然后找到一个设置,大概是允许本机访问,否则无法与jenkins通信

部署jenkins

1.不建议安装最新版,下面的镜像比较稳定

docker run -u root -d -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock --name jenkins jenkins/jenkins:2.479.3-lts

进入localhost:8080,初始密钥通过docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword获得 先选择安装推荐的插件,进入dashboard

2.安装完成后,点击系统管理-系统设置,定位到Jenkins Location Jenkins URL把localhost:8080修改为本机ip,例如http://192.168.1.100:8080

测试一:GitLab新推送邮件通知

这个测试实现了GitLab在新推送代码时,通过jenkins发送邮件通知 该测试需要有一个SMTP服务器,这里推荐免费的https://www.mailersend.com/

GitLab推送触发jenkins任务

1.首先在jenkins系统管理-插件管理中安装generic-webhook-trigger插件并重启 新建任务-构建一个自由风格的软件项目,暂且命名为jenkins_test_smtp 进入任务点击配置,在构建触发器下选择Generic Webhook Trigger 找到Token,随便填入一个字符串,建议用任务名jenkins_test_smtp 再找到Build Steps,添加一个Execute shell,内容如下

echo "new commit..."

这可以确保在新推送时,jenkins会执行这个shell脚本,就算不发送邮件我们也能在任务的控制台看到输出,这一步好了记得保存

2.在gitlab上打开你的项目,在设置中找到Webhooks,添加一个webhook 名称随便取一个jenkins_test_smtp URL填写http://<Jenkins URL>:8080/generic-webhook-trigger/invoke?token=<你刚刚填的Token> Trigger选择推送事件 可以点击下方的“测试”按钮测试是否能触发jenkins任务,如果提示“Hook executed successfully: HTTP 200”则说明配置成功 image11.png 这时候来到jenkins任务的控制台,观察是否有new commit...的输出 image12.png

jenkins自动发送邮件

1.首先在jenkins系统管理-插件管理中安装Email Extension插件并重启 先要预先配置好smtp的凭证,打开系统管理-凭据管理,添加一个全局凭据 类型是"Username with password",用户名和密码填写你自己的smtp账号和密码, 随便设一个id后面要用smtp-id image13.png 点开系统管理-系统设置,定位到Jenkins Location,系统管理员邮件地址设为你的smtp账号 同样的页面定位到Extended E-mail Notification SMTP服务器设为你自己的smtp服务器厂商,例如smtp.mailersend.net SMTP端口设为你的smtp端口,例如587 点开高级,SMTP凭证设为你之前创建的凭证smtp-id 注意根据你的smtp服务商的协议具体选择'SSL'或'TLS'或'OAuth2' image14.png

2.让我们重新回到jenkins_test_smtp任务的配置 找到构建后操作,添加一个Editable Email Notification Project From设置为你的smtp账号 Project Recipient List填入收信人邮箱,多个中间用英文逗号分隔 Content Type看你发送什么内容而定 Default Subject标题 Default Content内容 Advanced Settings打开,找到Trigger,里面新增触发器,一般测试选择Success,这会在构建任务成功后发送邮件,注意Send To设为Recipient List 保存后现在就可以推送一次看看效果

webhook读取变量填充到邮件

重新定位回构建触发器的Generic Webhook Trigger下方 我们可以依次添加Variable-Expression如下,这能从githab的请求中读取到值并存到当前变量中 project_name-$.project.name branch-$.ref user_name-$.user_name commit_msg-$.commits[0].message 然后修改邮件的标题

 仓库 ${project_name} 有新的推送消息

以及邮件的正文,注意Content Type设为HTML

<!DOCTYPE html>
<html>
<head>
<style>
    .card {
        font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
        max-width: 500px;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        overflow: hidden;
        color: #24292e;
    }
    .header {
        background-color: #2f363d;
        color: #ffffff;
        padding: 15px;
        font-size: 18px;
        font-weight: bold;
    }
    .content {
        padding: 20px;
        line-height: 1.6;
    }
    .label {
        color: #586069;
        font-size: 12px;
        text-transform: uppercase;
        margin-bottom: 4px;
        display: block;
    }
    .value {
        font-size: 15px;
        margin-bottom: 15px;
        display: block;
        font-weight: 500;
    }
    .commit-box {
        background-color: #f6f8fa;
        border-left: 4px solid #0366d6;
        padding: 10px;
        font-family: monospace;
        white-space: pre-wrap;
    }
    .footer {
        background-color: #f6f8fa;
        padding: 10px 20px;
        font-size: 12px;
        color: #6a737d;
        text-align: right;
    }
</style>
</head>
<body>
    <div class="card">
        <div class="header">
            🚀 Git 推送通知
        </div>
        <div class="content">
            <span class="label">项目仓库</span>
            <span class="value"><strong>${project_name}</strong></span>
            <span class="label">推送分支</span>
            <span class="value"><code>${branch}</code></span>
            <span class="label">推送人员</span>
            <span class="value">${user_name}</span>
            <span class="label">提交信息</span>
            <div class="commit-box">${commit_msg}</div>
        </div>
        <div class="footer">
            来自 Jenkins 自动化系统
        </div>
    </div>
</body>
</html>

最后实现的效果如下 image15.png

测试二:GitLab推送同步到GitHub

获取凭证

1.首先需要获取与gitlab的凭证,我们进入jenkins的终端 docker exec -it jenkins bash 通过指令生成私钥与公钥 ssh-keygen -t rsa -b 4096 -C "jenkins-sync" 查看公钥,复制到gitlab个人设置的SSH Keys中 cat ~/.ssh/id_rsa.pub image-30.png 然后先退出容器终端,并将私钥下载下来 docker cp jenkins:/root/.ssh/id_rsa ./id_rsa_backup

在jenkins系统管理-凭据管理中添加一个全局凭据,我命名它为gitlab-ssh-id 类型是"Secret file",文件上传你下载的私钥 这样我们就能正常对gitlab仓库进行git操作了

2.现在要获取与github的凭证,点击个人设置找到Developer settings 其中有Personal access tokens,点击生成一个新的token(classic),要选择repo权限 image-31.png

然后我们返回jenkins系统管理-凭据管理中添加一个全局凭据,我命名它为github-id 类型是"Username with password",用户名是你自己的github账号,密码就是你复制的token 这样我们就能正常对github仓库进行git操作了

配置同步任务

这次以流水线(pipeline)的方式创建任务, 依旧选择webhook触发,触发器的配置过程就不赘述了 但是触发器得配置这两个变量才能拿到仓库和分支的路径 gitlabSourceRepoHttpUrl-$.repository.git_http_url ref-$ref

流水线的脚本定义如下

pipeline {
    agent any

    environment {
        // 这里替换为你的gitlab ip
        GL_HOST = "192.168.8.246"
        // 这里替换为你的gitlab ssh端口(不是web端口)
        GL_SSH_PORT = "9922" 
        // 这里替换为你的github仓库地址
        GH_REPO_BASE = "github.com/SugarSong404/jenkins_test.git"
    }

    stages {
        stage('同步仓库') {
            steps {
                    withCredentials(
                        //这里的gitlab-ssh-id是我们之前添加的全局凭据
                        [file(credentialsId: 'gitlab-ssh-id', variable: 'SSH_KEY'),
                        //这里的github-id也是我们之前添加的全局凭据
                        usernamePassword(credentialsId: 'github-id', usernameVariable: 'GH_USER', passwordVariable: 'GH_TOKEN')]) {
                        sh """
                            mkdir -p ~/.ssh
                            
                            ssh-keyscan -p ${env.GL_SSH_PORT} ${env.GL_HOST} >> ~/.ssh/known_hosts
    
                            chmod 400 \${SSH_KEY}
                        
                            export GIT_SSH_COMMAND="ssh -i \${SSH_KEY} -o IdentitiesOnly=yes"

                            PROJECT_PATH=\$(echo "${gitlabSourceRepoHttpUrl}" | sed 's|http://[^/]*/||')
                            
                            GL_SSH_URL="ssh://git@${GL_HOST}:${GL_SSH_PORT}/\${PROJECT_PATH}"
                            
                            echo ">>> 开始从 GitLab 克隆 (SSH): \$GL_SSH_URL"

                            rm -rf sync_tmp.git
                            git clone --mirror \$GL_SSH_URL sync_tmp.git
                            cd sync_tmp.git

                            GH_PUSH_URL="https://${GH_USER}:${GH_TOKEN}@${GH_REPO_BASE}"
                            
                            echo ">>> 正在推送到 GitHub (Token)..."
                            git push --mirror \$GH_PUSH_URL
                        """
                    }
                }
        }
    }

    post {
        always {
            deleteDir()
        }
        success {
            echo "同步成功!"
        }
        failure {
            echo "同步失败,请检查 Jenkins 日志。"
        }
    }
}

其实就是在jenkins中先克隆gitlab仓库,然后原样推送到github仓库 运行的结果如下 image-32.png 现在每次推送到gitlab中的代码都会自动同步到github仓库中

测试三:对web代码进行测试的Agent

配置Agent

jenkins还可以将复杂的任务分布到多个node上进行分布式执行,这些node可以部署在不同的机器上,它们一般被称为slave(旧称)或agent


现在我们部署一个专门将仓库的推送进行测试的Agent(还是部署在本地) 具体操作是jenkins系统管理-节点和云管理中添加一个新的节点, 我命名它为agent-node,添加为固定节点 启动方式为"Launch agents via Java Web Start", label(标签)是你Pipeline中使用这个agent的标签,我同样设为了agent-node 一般远程工作目录配置的是/home/jenkins/agent image-40.png 保存后点进agent可以看到上图一些快捷启动指令, 我们不用这些指令启动而是用docker的jenkins/inbound-agent便捷部署 但是要记一下其中的一些字段 比如url,agent-name,agent-secret,agent-dir


jenkins/inbound-agent不带有npm与node,所以需要自己写下dockerfile,可以换个源快一点

FROM jenkins/inbound-agent

USER root

RUN sed -i 's|http://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources \
 && sed -i 's|http://security.debian.org|https://mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list.d/debian.sources \
 && apt-get update \
 && apt-get install -y nodejs npm \
 && rm -rf /var/lib/apt/lists/*

USER jenkins

构建一下镜像 docker build -t jenkins-agent-node . 然后运行

docker run -d \
--name jenkins-agent-build \
-e JENKINS_URL=<url> \
-e JENKINS_AGENT_NAME=<agent-name> \
-e JENKINS_SECRET=<agent-secret> \
-e JENKINS_WEB_SOCKET=true \
-v jenkins-agent-build:<agent-dir> \
jenkins-agent-node

然后日志中就可以看到连接成功 image-41.png

简单测试项目以及Pipeline

//__test__/test.app.js
const request = require('supertest')
const app = require('../app')

describe('GET /', () => {
  it('responds with 200 and correct message', async () => {
    const res = await request(app).get('/')
    expect(res.statusCode).toBe(200)
    expect(res.text).toBe('Hello, Node.js CI/CD with Jest!')
  })
})
//app.js
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.send('Hello, Node.js CI/CD with Jest!')
})

module.exports = app
//server.js
const app = require('./app')

app.listen(3000, () => {
  console.log('Server running on port 3000')
})
//pacakge.json
{
  "name": "jenkins_test_node",
  "version": "1.0.0",
  "description": "jenkins_test_node",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pm2": "^6.0.14"
  },
  "devDependencies": {
    "jest": "^29.6.1",
    "supertest": "^6.3.3"
  }
}

配置下方的pipline,建立好webhook,推送上方的项目,即可完成测试

pipeline {
    // 切换为专门的Agent
    agent { label 'agent-node' }
    stages {
        // 这种语法(来自Git Plugin)能从git仓库中自动检出代码到本机
        // 这里我用了gitlab的access tokens来建立连接
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitLab...'
                git url: 'http://tangsong404:glpat-lvSloDT6rtlX1Wr3hwYj4286MQp1OnkH.01.0w0fc114j@192.168.8.246:9980/tangsong404/test_jenkins.git', branch: 'master'
            }
        }
        stage('Install') {
            steps {
                echo 'Installing dependencies...'
                sh 'npm install'
            }
        }
        stage('Test') {
            steps {
                echo 'Running tests...'
                sh 'npm test'
            }
        }
    }
    post {
        success {
            echo '恭喜!流水线跑通了!'
        }
        failure {
            echo '出错了,请检查上面的日志。'
        }
    }
}

image-42.png 和直接用一整套Stage相比,多 Agent Pipeline 的优势在于:不同 stage 可以跑在不同主机上,环境隔离、互不干扰;可以并行利用资源加快构建;支持不同语言和工具链分离;实现权限隔离和故障隔离;易于横向扩展,并能跨操作系统或动态创建临时 agent,使 CI 高效、安全且可扩展。