借助 Jenkins(Coding) 实现持续集成

2022-07-28, 星期四, 07:33

DevOps培训CI/CD

在开发过程中总有一部分工作是机械化、乏味、易出错的,比如打包和部署工作。将部署与发布交给持续集成,把时间花在更有价值的事物上(或者浪费到别的事情上)。

Coding 借助 Jenkins 提供了持续集成能力,可以通过 Web 端图形编辑器配置 CI 流程。不过考虑到可评审、可迭代、可归档、可归档与可复现需求(或者说基础设施代码化最佳实践),建议在调试完成后把配置项保存为 Jenkinsfile 文件并检入项目代码库,然后配置使用代码库中的 Jenkinsfile 执行构建。

2023-04-13 更新:由于一些非技术层面的考虑,关于 Jenkins 本身的内容移动到 使用公司自建 Jenkins、Gogs、Registry 实现持续集成 中。

agent 为流水线指定了一个工作区,agent any 将会使用 Coding 的默认构建环境。

SDK 与工具 版本
java 1.8.0_191
nodejs 10
python3/pip3 3.9、3.8、3.7
go 1.14.4
maven 3.6.3
yarn 1.15.2
gradle 4.10.2
git 2.28.0
git-lfs 2.7.2
docker 20.10.6
docker-compose 1.26.0

如果 Coding 提供的默认 SDK 版本不符合需求(如需要使用 JDK 17 或 nodejs:16),可以参考相应文档在 Docker 容器内构建。

流水线使用 environment {} 指令设置环境变量,也可以在 Coding 端配置环境变量,在流水线中使用 ${VARNAME} 引用。

案例:打包 Java 应用程序并制作 Docker 镜像

使用 Vert.X 框架编写了一个应用程序 http-listener(链接移除),该程序监听 8888 端口从控制台输出接收到的 HTTP 请求。由于 Coding 基础构建环境提供的 JDK 版本为 1.8.0_191,无法编译 JDK 11 项目,因此构建工作在 maven:3-eclipse-temurin-11 容器内进行。

构建流水线由如下几步构成:

flowchart LR checkout(检出代码) --> build(编译与打包) --> bundle(制作 Docker 镜像并提交到制品库)

检出使用一套样板代码,所有项目都差不多,不做赘述。

stage('Build') {
  agent {
    docker {
      image 'maven:3-eclipse-temurin-11'
      reuseNode true
      args '-v /root/.m2:/root/.m2'
    }
  }
  steps {
    sh 'mvn -Ddockerfile.skip=true clean package'
  }
}

构建阶段通过 agent { docker { ... }} 声明构建环境。image 'maven:3-eclipse-temurin-11' 声明要使用的镜像,也可以通过指定 Dockerfile 构建自己的编译容器。reuseNode true 要求在流水线顶层指定的节点上运行该容器,使用同一个工作区。由于容器启动的时候都是干净的,每次构建中 Maven 都会从中央仓库下载依赖,这会极大拖慢流水线的速度,通过 args 参数要求容器挂载 /root/.m2/ 目录,可以缓存依赖,节省之后构建的时间(需要在 Coding 端配合配置缓存目录)。

如果需要在容器节点内继续使用 Docker 命令,则需要 -v /var/run/docker.sock:/var/run/docker.sock-v /usr/bin/docker:/usr/bin/docker

mvn package 的打包结果类似 http-listener-1.0.0-fat.jar,其中 1.0.0 来自 pom.xmlproject.version 。要在构建过程中读取该变量,需要使用插件或编写命令解析 pom.xml。推荐在实践过程中优先使用 git tag 而非 project.version 作为版本号迭代记录方案,在构建过程中手动配置环境变量 MAVEN_VERSION 帮助构建脚本组装制品文件名。

stage('Bundle') {
  steps {
    sh "docker build --build-arg JAR_FILE=target/${MAVEN_ARTIFACT_ID}-${MAVEN_VERSION}-fat.jar -t ${CODING_DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_VERSION} -f ${DOCKERFILE_PATH} ${DOCKER_BUILD_CONTEXT}"
    useCustomStepPlugin(key: 'SYSTEM:artifact_docker_push', version: 'latest', params: [image:"${CODING_DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_VERSION}",repo:"${DOCKER_REPO_NAME}",properties:'[]',project:'showcase',username:'${PROJECT_TOKEN_GK}',password:'${PROJECT_TOKEN}'])
  }
}

http-listener 项目在本地使用 com.spotify:dockerfile-maven-plugin 和 Dockerfile 创建镜像,插件在构建过程中依赖 configuration.buildArgs.JAR_FILE 获取制品文件名。为了复用 Dockerfile,通过 --build-arg 传入组装后的制品文件名。之后通过 SYSTEM:artifact_docker_push 插件把镜像推送到制品库。构建完成后可以在项目的制品仓库看到 Docker 镜像。

docker pull ********-docker.pkg.coding.net/showcase/default/http-listener:1.0.2 拉取 1.0.2 版本的镜像,可在本地测试执行。

可以设置持续集成在代码推送新标签时触发,这样 git push tag 或者在代码仓库的「版本管理」处手动创建新版本时就会触发构建。

需注意只有标签推送触发的 CI 流程才会存在 GIT_TAG 变量,可以在 Coding 端配置 ${GIT_TAG:-snapshot},这样由标签推送触发的构建会得到类似 http-listener:1.0.0 的制品,而手动、定时、远程触发构建且未指定标签时会得到 http-listener:snapshot

案例:使用 CI 实现数据自动更新的大屏

思路来自 GitHub Flat Data ,这是一款基于 GitHub Action 的数据仪表盘产品。用户初始化一个 Flat Data Workflow,使用 low-code 的方式定义获取(支持 HTTP 接口,CSV,SQL 连接等数据源)、处理、保存数据的方法,通过静态站点生成器创建数据分析面板。这一系列操作由 GitHub Action 定期执行实现数据更新。

在 ci-dashboard(链接移除)项目中利用 CI 定期从 全国新型肺炎疫情实时数据接口 获取浙江省新型肺炎数据,使用 Vue 和 echarts 展示疫情数据并绘制浙江省范围内的市级累计确诊人数热力图,生成页面后通过腾讯云对象存储托管实现静态网站服务(链接移除)。

项目使用 Vue-Cli 创建,结构如下:

.
├── Jenkinsfile
├── babel.config.js
├── ci
│   └── index.js
├── jsconfig.json
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── components
│   │   ├── InfoMap.vue
│   │   └── MiscInfo.vue
│   ├── dynamic
│   │   ├── cities.json
│   │   └── misc.json
│   ├── main.js
│   └── zhejiang-geo.json
└── vue.config.js

CI 将定期调用 ci/index.js 更新数据,数据保存在 src/dynamic/ 目录下的 JSON 文件中。

const result = await axios({
    method: 'get',
    url: `https://lab.isaaclin.cn/nCoV/api/area?province=${encodeURI('浙江省')}`,
}).then(res => res.data?.results?.[0]);
if (result === undefined) {
    throw new Error('没有数据');
}
const miscData = {
    currentConfirmedCount: result?.['currentConfirmedCount'] ?? 'N/A',
    // ...
    updateTime: result?.['updateTime'] ?? DateTime.local().toMillis(),
}
const citiesData = result?.['cities'] ?? [];
await fs.writeFile(path.join(__dirname, '../src/dynamic/misc.json'), JSON.stringify(miscData, null, 2), 'utf8');
await fs.writeFile(path.join(__dirname, '../src/dynamic/cities.json'), JSON.stringify(citiesData, null, 2), 'utf8');

src/components/ 中的组件引入数据并绘制图表。

<script>
import misc from '@/dynamic/misc.json';

export default {
  name: 'MiscInfo',
  data() {
    return {
      currentConfirmedCount: misc.currentConfirmedCount,
      confirmedCount: misc.confirmedCount,
      suspectedCount: misc.suspectedCount,
      curedCount: misc.curedCount,
      deadCount: misc.deadCount,
    }
  }
}
</script>

持续集成部分配置如下:

flowchart TD init([初始化构建环境]) --> checkout[检出代码] --> dep[安装依赖] --> build[拉取数据并构建项目] --> commit[提交数据变更] --> deploy[制品发布到对象存储]

在 CI 过程中更新了数据文件,通过 Shell 命令向代码库检入新数据。

stage('Commit & Push') {
  steps {
    sh "git config --global user.name 'repo-bot'"
    sh "git config --global user.email 'bot@yufanonsoftware.cc'"
    sh "git commit -am 'bot publish' && git push https://${PROJECT_TOKEN_GK}:${PROJECT_TOKEN}@e.coding.net/serverless-1000******1/ci-showcase/ci-dashboard.git HEAD:master || echo 'No changes to commit'"
  }
}

源数据会在每日上午 9:00-10:30 的时间段内更新,因此设置构建计划的触发规则为「星期天 星期一 星期二 星期三 星期四 星期五 星期六 / 12:00 单次触发」。由于构建涉及到向代码库检入数据触发代码提交钩子,关闭「代码源触发」功能。

2022 年 7 月 22 日晚访问:

次日上午 CI 触发了一次构建: