一个小型的开发团队会希望在许多工作中引入自动化,这包括:
- 发布版本构建制品
- 部署(特别是现在这种访问 ECS 特别麻烦的情况下)
- 回滚到旧版本
使用持续集成可以比较方便地解决问题 1,而问题 2 和问题 3 则可以依靠制品库让事情变得简单起来(特别是目前原则上不允许「持续部署」的情况下(原则上))。
本示例将向你展示以下内容:
- 使用声明式语法编写 Jenkinsfile
- 在特定环境的容器中构建制品
- 将制品打包为容器镜像并提交到制品库
- 利用 Webhook 配置自动化构建流程,允许用户通过提交 git tag 触发上述工作
使用 Dorian 的代码作演示之用,这是一个公开项目,可在公司内部 Gogs 上获取,结构如下:
.
├── Dockerfile
├── Dockerfile-rootful
├── Jenkinsfile
├── pom.xml
├── settings.xml
├── src
└── target
├── dorian-0.0.1-SNAPSHOT.jar
...
其中 Dockerfile
用于构建 rootless 容器,如果生产环境使用的是早期版本的 Docker,则推荐使用 Dockerfile-rootful
,后者出于安全考虑在镜像中定义了专门的用户 app
。
settings.xml
的作用与 $HOME/.m2/settings.xml
相同,在执行 mvn
命令时使用 --settings ./settings.xml
参数可以覆盖默认配置,实现不同项目的仓库配置隔离。
pom.xml
中有使用 org.codehaus.mojo:exec-maven-plugin
实现 mvn package
默认打包容器镜像的操作。不过在本示例中我们会单独执行制作镜像的步骤,因此不会用到这个插件。
Jenkinsfile
是使用声明式语法编写的 Jenkins pipeline 定义文件,符合如下结构:
pipeline {
agent any
environment {
// set env vars
}
stages {
stage {
steps {
// do some ops
...
}
}
stage {
steps { ... }
} ...
}
}
编写 Jenkinsfile
完整的 Jenkinsfile 见内部 Gogs 上的 Dorian 项目。
Step 1. 定义与初始化环境变量
environment {
// 注意,这是一篇公开文章
// registry 地址等信息做了化名处理,实际使用请参考项目代码
GIT_REPOSITORY = 'https://gogs.ddrpa.cc/yufan/dorian.git'
PROJECT_NAME = 'dorian'
MAVEN_ARTIFACT = 'dorian-0.0.1-SNAPSHOT.jar'
DOCKERFILE_PATH = 'Dockerfile'
DOCKER_BUILD_CONTEXT = '.'
DOCKER_REGISTRY_HOST = 'registry.ddrpa.cc'
IMAGE_NAME = "${PROJECT_NAME.toLowerCase()}"
}
在这里设置环境变量等内容,如果其他 Java 项目想使用类似的构建流程,只需修改 Git 地址和 Java 制品名称等变量。
IMAGE_VERSION = "${REF?:'latest'}"
GIT_CHECKOUT_BRANCH = "${REF?'refs/tags/'+REF:'*/master'}"
你可能会问 MAVEN_ARTIFACT
为什么也写成一个常量了?因为要在构建镜像阶段获取 project.version
需要手动解析 pom.xml
,这太麻烦了,而且反正我们用 git tag 来标记版本号了,pom 里那个就不重要了。
开发人员推送一个新的 git tag 时,Gogs 将会向 Jenkins 发送请求,根据下文将会提到的配置,构建环境中会出现值等于 tag name
的 REF
变量,设置 IMAGE_VERSION=${REF}
和 GIT_CHECKOUT_BRANCH=refs/tags/${REF}$
。
在 Jenkins 上手动触发构建时则不会初始化 REF
变量,因此这两个变量的赋值使用了三元表达式,当 REF
未定义时使用默认值 latest
和 */master
。
如果是非公开项目,需要在 Jenkins 中预先配置好 credentials,可以把 ID 也写入配置文件。
GIT_CREDENTIALS_ID = 'gogs-yufan'
Step 2. 检出代码
使用 GitSCM 插件检出代码。如果是非公开项目,在 userRemoteConfigs
中添加 credentialsId
配置:
stage("Checkout") {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: "${GIT_CHECKOUT_BRANCH}"]],
extensions: [],
userRemoteConfigs: [[
// credentialsId: "${GIT_CREDENTIALS_ID}",
url: "${GIT_REPOSITORY}"]]])
}
}
检出路径为 $HOME/.jenkins/workspace/dorian-starter-pack
,大部分情况下不需要关注这个信息。
Step 3. 构建 Java 制品
stage("Build jar file") {
agent {
docker {
image 'docker.io/library/maven:3-eclipse-temurin-17'
reuseNode true
args '-v $HOME/.m2:/root/.m2'
}
}
steps {
sh 'mvn --settings ./settings.xml -Ddockerfile.skip=true clean package'
}
}
项目使用 Spring Boot 3.0 框架,依赖 JDK 11 及以上版本,而构建节点本身只安装了 JDK 8,因此需要在 JDK 11+ 版本的 Maven 容器中构建。本系统已经安装了 Docker 插件,使用 agent docker
可以指定一个容器节点。
你可能有注意到我使用了 args '-v $HOME/.m2:/root/.m2'
挂载 $HOME/.m2
目录,此外 Jenkins Docker 插件会自动挂载当前的工作空间(也就是 /root/.jenkins/workspace/dorian-starter-pack
),因此不需要在容器内执行检出代码的操作,待会也不需要把构建好的 Java 制品移出容器。
为了加速 Maven 下载,我使用了华为云的镜像,这个配置通过项目根目录下的 settings.xml
和 mvn
命令中的 --settings ./settings.xml
参数引入。此外还通过-Ddockerfile.skip=true
跳过了 Maven 插件构建镜像的步骤。
Step 4. 构建容器镜像
stage("Build container image") {
steps {
sh "docker build --build-arg JAR_FILE=target/${MAVEN_ARTIFACT} --tag ${IMAGE_NAME}:${IMAGE_VERSION} --file ${DOCKERFILE_PATH} ${DOCKER_BUILD_CONTEXT}"
}
}
使用 docker build
构建镜像,与本地操作没有什么区别。
Step 5. 上传到制品库
已经事先在 Jenkins 中通过 credentials 配置了 Registry 的凭证,直接在 environment
中引用:
stage("Upload to registry") {
environment {
REGISTRY_CREDS = credentials('docker-registry-credentials')
}
steps {
sh('docker login $DOCKER_REGISTRY_HOST -u $REGISTRY_CREDS_USR -p $REGISTRY_CREDS_PSW')
sh "docker tag ${IMAGE_NAME}:${IMAGE_VERSION} ${DOCKER_REGISTRY_HOST}/${IMAGE_NAME}:${IMAGE_VERSION}"
sh "docker push ${DOCKER_REGISTRY_HOST}/${IMAGE_NAME}:${IMAGE_VERSION}"
}
}
由于 docker-registry-credentials
是一个「用户名 - 密码」格式的凭据,credentials()
函数的输出赋值给 REGISTRY_CREDS
后会创建两个变量 —— REGISTRY_CREDS_USR
和 REGISTRY_CREDS_PSW
,内容就和字面意思一样。
公司自建的镜像 Registry 没有像 Docker Hub 那样提供浏览的功能,因此镜像的版本得自己记着(这就是为什么 git tag 会复用到镜像的 tag 上(如果你上 Nexus 去找的话也不是不可以))。
由于 latest
标签是允许覆盖的,你比较勇的话也可以一直打 latest
版本。
这里使用的 sh()
函数是一个 Groovy 方法,环境变量在函数内部才被解释为真正的值,可以防止在构建日志中泄漏秘密。
在 Jenkins 上新建 pipeline 流水线
没什么好说的,新建项目,直接转到 Pipeline 配置,把刚才写的 Jenkinsfile 的内容粘贴到 Script 一栏。
你也可以设置 Jenkins 自动读取项目代码中的 Jenkinsfile。
设置 Webhook 自动触发构建
在 Gogs 上设置
当开发者创建分支或标签时,Gogs 会向指定的 URL 发送 HTTP POST 请求。
由于 Jenkins 上所有项目都通过同一个地址(JENKINS_URL/generic-webhook-trigger/invoke
)触发构建,我们需要为指定项目生成一个标识符,通过 URL 中的 token
查询参数传递。
由于我们希望通过 git tag 管理版本发布,设置只有创建分支或标签的时候触发 Webhook。
一旦 Gogs 发起过 Web 钩子调用,在配置页面下方就会出现调用历史记录,可以查看 Gogs 发送和接收了什么数据,用来辅助调试(还可以再次发送该请求)。
开发者通过 git tag -a 0.0.1 -m "git tag test" && git push origin --tags
创建并推送了 0.0.1
标签,在 Webhook 配置界面下方可以看到触发了一次事件。
请求体内容摘录如下:
{
"ref": "0.0.1",
"ref_type": "tag",
"default_branch": "master",
...
}
在项目的 Gogs 主页也可以看到有一个 0.0.1
版本产生了。
在 Jenkins 上设置
在项目的 Configure -> General 中勾选 Generic Webhook Trigger。
点击 Post content parameters 中的 Add 按钮,添加 Webhook 请求体的解析逻辑。之前设置了 Webhook 请求体类型为 application/json
,Jenkins 可以通过 JSONPath 指定如何解析 JSON 结构取值并赋给相应变量。
设置变量 REF_TYPE
由 $.ref_type
赋值,然后通过 Optional filter 配置当 REF_TYPE
的值匹配正则表达式 ^(tag)$
时才触发构建,这样新建分支事件时就不会触发构建了。
同理设置 $REF = $.ref(= '0.0.1')
,在这里创建的变量可以在 pipeline 中使用,用来初始化其他变量。
在 Token 一栏填写之前生成的唯一标识符。
关于版本号的建议
参考 语义化版本 2.0.0 这篇文章:
版本格式:X.Y.Z,版本号递增,规则如下:
- 主版本号 X:当你做了不兼容的 API 修改,
- 次版本号 Y:当你做了向下兼容的功能性新增,
- 修订号 Z:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到「X.Y.Z」的后面,作为延伸。
从制品库拉取镜像
拉取镜像:
podman login registry.ddrpa.cc -u jack-the-dog -p <registry-password>
podman pull registry.ddrpa.cc/dorian:0.9.4
使用 Rootless 容器是一个比较好的安全习惯:
podman run --userns=keep-id --rm \
-p 8888:8080 \
-e DB_HOST=<db-host> \
-e DB_PASSWORD=<db-root-password> \
registry.ddrpa.cc/dorian:0.9.4
curl -v http://localhost:8888/ship