使用 CNB 构建并部署自己的项目
本文介绍了如何使用腾讯云 CNB(Cloud Native Build)构建和部署 Maven 项目,通过 .cnb.yml 文件配置流水线任务,结合 YAML 锚点与并行任务机制,实现对 Maven 项目的自动化构建与部署。
前言
CNB(Cloud Native Build) 是腾讯云 CODING 团队推出的全新产品 "云原生构建",对标 GitHub;内置加速服务,可快速访问 GitHub、DockerHub 等资源;还能在 Pipeline 中将代码同步至其他平台,非常适合国内开发者。CNB 基于 Docker 生态,与 Github 等平台类似,开发者通过编写 yml
文件声明自己的流水线。
本文将以 maven 项目为例介绍在 cnb 上自定义一个简单的流水线。
准备工作
流程分析
一个简化的流程如下:
示例项目结构
假如你的项目结构是这样子的:
Demo带有两个子项目
Demo-Backend后台
src
main
java
…
resources
application.yml
…
pom.xml
Demo-Frontend前台
src
main
java
…
resources
application.yml
…
pom.xml
docker
Demo-Backend
application.yml要部署的环境的配置
Demo-Frontend
application.yml要部署的环境的配置
Dockerfile
cache.Dockerfile
deploy.sh部署脚本
entrypoint.sh
pom.xml
.gitignore
README.md
…
示例流水线文件
此处需注意yml语法,详见 YAML 语言教程- 阮一峰的网络日志
.cnb.yml
文件
在根目录创建 .cnb.yml
文件如下(可暂时跳转到分步说明阅读详细步骤):
其中 &
代表锚点, *
代表别名,可以用来引用; &
用来建立锚点,<<
表示合并到当前数据,*
用来引用锚点。
.cnb.yml
文件 (点击展开)
# .cnb.yml
# ----------------------
# 各阶段 Job 定义模板
# ----------------------
# 准备阶段
prepare: &prepare
script: |
POMS=$(find . -name "pom.xml" | sort | paste -sd "," -)
if [ "${CNB_IS_TAG}" = "true" ]; then
TAG1=${CNB_BRANCH}
TAG2=latest
else
TAG1=${CNB_BRANCH}-${CNB_COMMIT_SHORT}
TAG2=${CNB_BRANCH}
fi
printf "##[set-output poms=%s]\n" "$POMS"
printf "##[set-output tag1=%s]\n" "$TAG1"
printf "##[set-output tag2=%s]\n" "$TAG2"
exports:
poms: POMS
tag1: TAG1
tag2: TAG2
#构建缓存镜像
docker_cache: &docker_cache
image: maven:3.9.9-eclipse-temurin-17
type: docker:cache
options:
dockerfile: ./docker/cache.Dockerfile
by: $POMS
exports:
name: DOCKER_CACHE_IMAGE
# Maven 打包
maven_package: &maven_package
image: $DOCKER_CACHE_IMAGE_NAME
volumes:
- /root/.m2:copy-on-write
script: |
mvn versions:set -DnewVersion=${TAG1} -DgenerateBackupPoms=false
mvn clean -B package -DskipTests
# Docker build && Docker push
build_push: &build_push
script: |
IMAGE_BASE=${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/$ {ARTIFACT_ID_LOWERCASE}
docker build -f docker/Dockerfile -t ${IMAGE_BASE}:${TAG1} -t $ {IMAGE_BASE}:${TAG2} --build-arg ARTIFACT_ID=${ARTIFACT_ID}--build-arg VERSION=${TAG1} .
docker push ${IMAGE_BASE}:${TAG1}
docker push ${IMAGE_BASE}:${TAG2}
# SSH 部署
ssh_deploy: &ssh_deploy
image: docker.cnb.cool/falling42/ssh-deploy:v0.1.0
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
settings:
use_screen: 'no'
use_jump_host: 'no'
ssh_host: ${your_ssh_host}
ssh_user: ${your_ssh_user}
ssh_private_key: ${your_ssh_private_key}
execute_remote_script: 'yes'
transfer_files: 'no'
copy_script: 'yes'
source_script: './docker/deploy.sh'
deploy_script: '/opt/ops/deploy-demo.sh'
service_name: ${ARTIFACT_ID}
service_version: "${TAG1}"
#失败通知:wechat-bot
notify_wechat_bot: ¬ify_wechat_bot
image: tencentcom/wecom-message
settings:
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenvyml
robot: ${your_webhook_url}
msgType: text
content: |
🚨构 建 失 败 通 知🚨
📦仓 库: ${CNB_REPO_SLUG_LOWERCASE}
👤发 起 人: ${CNB_BUILD_USER}
🛠️失败任务: ${CNB_BUILD_FAILED_STAGE_NAME}
👉查看详情: ${CNB_BUILD_WEB_URL}
#失败通知:wechat
notify_wechat: ¬ify_wechat
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
image: clem109/drone-wechat
settings:
corpid: ${your_corpid}
corp_secret: ${your_corp_secret}
agent_id: ${your_agent_id}
to_user: ${your_user_id}
msg_url: ${CNB_BUILD_WEB_URL}
safe: 0
btn_txt: 查看详情
title: ${CNB_REPO_SLUG_LOWERCASE} 构建失败通知
description: "发起人: ${CNB_BUILD_USER}\n失败任务: $ {CNB_BUILD_FAILED_STAGE_NAME}\n点击查看详情: ${CNB_BUILD_WEB_URL}\n"
# 失败通知:serverchan
notify_serverchan: ¬ify_serverchan
image: yakumioto/drone-serverchan
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
settings:
key: ${your_sct_key}
text: 🚨 构建失败通知
desp: |
> **📦 仓库:** ${CNB_REPO_SLUG_LOWERCASE}
> **👤 发起人:** ${CNB_BUILD_USER}
> **🛠️ 失败任务:** ${CNB_BUILD_FAILED_STAGE_NAME}
> **[👉 点击查看完整构建日志](${CNB_BUILD_WEB_URL})**
# 失败通知:email
notify_email: ¬ify_email
image: drillster/drone-email
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
settings:
host: ${your_smtp_host}$
port: 465
recipients: ${CNB_COMMITTER_EMAIL}
username: ${your_smtp_user}
password: ${your_smtp_password}
from.address: ${your_smtp_user}
from.name: CNB构建通知
subject: ${CNB_REPO_SLUG_LOWERCASE} 构建失败通知
body: |
<div style="font-family: Arial, sans-serif; padding: 12px; border:1px solid #eee;">
<h2 style="color: #D32F2F;">🚨 构建失败通知</h2>
<p><strong>📦 仓库:</strong> ${CNB_REPO_SLUG_LOWERCASE}</p>
<p><strong>👤 发起人:</strong> ${CNB_BUILD_USER}(ID: $ {CNB_BUILD_USER_ID})</p>
<p><strong>🛠️ 失败任务:</strong> ${CNB_BUILD_FAILED_STAGE_NAME}<p>
<p style="margin-top: 20px;">
👉 <a href="${CNB_BUILD_WEB_URL}" style="color: #1976D2; text-decoration: none;">
点击查看完整构建日志</a>
</p>
<hr style="margin-top: 24px;"/>
<p style="font-size: 12px; color: #888;">
来自 CNB构建通知
</p>
</div>
# 定义要并行构建的模块
Frontend: &Frontend
ARTIFACT_ID: Demo-Frontend
ARTIFACT_ID_LOWERCASE: demo-frontend
Backend: &Backend
ARTIFACT_ID: Demo-Backend
ARTIFACT_ID_LOWERCASE: demo-backend
# ----------------------
# 并行 Jobs 定义模板
# ----------------------
build_push_jobs: &build_push_jobs
Frontend: { <<: *build_push, env: { <<: *Frontend } }
Backend: { <<: *build_push, env: { <<: *Backend } }
deploy_jobs: &deploy_jobs
Frontend: { <<: *ssh_deploy, env: { <<: *Frontend } }
Backend: { <<: *ssh_deploy, env: { <<: *Backend } }
# ----------------------
# 主 Pipeline 定义模板
# ----------------------
# ----------------------
pipeline: &pipeline
name: Demo
runner:
tags: cnb:arch:amd64
cpus: 4
services:
- docker
stages:
- name: prepare
<<: *prepare
- name: build cache image
<<: *docker_cache
- name: maven package
<<: *maven_package
- name: build push
jobs:
<<: *build_push_jobs
- name: ssh deploy
jobs:
<<: *deploy_jobs
failStages:
- name: notify
jobs:
notify-email:
<<: *notify_email
notify-wechat:
<<: *notify_wechat
notify-wechat-bot:
<<: *notify_wechat_bot
notify-serverchan:
<<: *notify_serverchan
# ----------------------
# 分支触发定义
# ----------------------
main:
push:
- <<: *pipeline
"**":
web_trigger_one:
- <<: *pipeline
$:
tag_push:
- <<: *pipeline
.cnb/web_trigger.yml
文件
在 项目根目录创建 .cnb/web_trigger.yml
文件,用于给云原生构建的项目首页配置构建按钮
注意event
的名字要与 .cnb.yml
文件中的触发条件相对应
更多个性化配置详见 手动触发流水线
# .cnb/web_trigger.yml
branch:
- buttons: # 自定义按钮
- name: 手动构建
description: 手动构建
event: web_trigger_one
分步说明
一个流水线的执行过程是:
仓库发生事件 -> 确定所属分支 -> 确定事件名 -> 执行流水线 -> 执行任务 -> 失败时的任务
触发事件
这部分算是整个流水线的入口,决定何时触发流水线任务
更多触发事件请查看 触发事件
# Branch 事件:远端代码分支变动触发的事件。
main: # 分支名称
push: # git 仓库事件,分支 push 时触发。
- <<: *pipeline # 流水线配置
## 注意这个'-' 因为将pipeline合并到当前数据时候pipeline的配置无'-',这里不加'-'则无法解析流水线任务
# web_trigger 自定义事件
"**": # ** 代表匹配所有分支名
web_trigger_one: # 名字随意,不重复即可
- <<: *pipeline
# Tag 事件:由远端代码和页面 Tag 相关操作触发的事件。
$: # 对所有 tag 生效
tag_push: # 页面或者 git 创建并推送新 tag 时触发
- <<: *pipeline
流水线配置
这部分包括配置需要的构建环境以及配置流水线步骤
更多配置请查看 Pipeline
pipeline: &pipeline
name: Demo #流水线名字
# --- 构建环境配置 ---
runner:
tags: cnb:arch:amd64 # 指定使用具备哪些标签的构建节点。例如cnb:arch:arm64:v8
cpus: 4 # 指定构建需使用的最大 cpu 核数(memory = cpu 核数 * 2 G)
services: # 用于声明构建时需要的服务,格式:name:[version], version 是可选的。
- docker # 用于开启 dind 服务,当构建过程中需要使用 docker build,docker login 等操作时声明, 会自动在环境注入 docker daemon 和 docker cli。
# --- 构建环境配置 结束---
# --- 执行任务(s) ---
stages: # 定义一组阶段任务,每个阶段串行运行。Stage 表示一个构建阶段,可以由一个或者多个 Job 组成。每一个"-"开头就代表一个stage(在yml中一组连词线开头的行,构成一个数组。)
- name: prepare # 只有一个 Job,省略 "jobs:"(省掉 Stage 直接书写这个 Job)
<<: *prepare
- name: build cache image
<<: *docker_cache
- name: maven package
<<: *maven_package
- name: build push
jobs: # 定义一组任务,每个任务串行/并行运行。本文这里使用并行运行。请见后面的说明。
<<: *build_push_jobs
- name: ssh deploy
jobs: # 定义一组任务,每个任务串行/并行运行。本文这里使用并行运行。请见后面的说明。
<<: *deploy_jobs
# --- 执行任务(s) 结束 ---
# --- 在执行任务失败时执行的任务 ---
failStages: # 定义一组失败阶段任务。当正常流程失败,会依次执行此阶段任务。
- name: notify
jobs: # 定义一组任务,每个任务串行/并行运行。本文这里使用并行运行。当值为对象(无序)时,那么这组 Job 会并行执行。(对象的一组键值对,使用冒号结构表示。)
notify-email:
<<: *notify_email
notify-wechat:
<<: *notify_wechat
notify-wechat-bot:
<<: *notify_wechat_bot
notify-serverchan:
<<: *notify_serverchan
env
由于本文的示例项目使用了两个子项目,为了避免重复配置,本文使用了 yml 的引用结合 CNB 的 env 来声明要构建的多个项目。如果只是一个单体项目,按照此逻辑直接声明一个锚点即可。
# 定义要构建的项目, 下方的yml对象声明了每个项目的env,便于在后续流程中使用
Frontend: &Frontend
ARTIFACT_ID: Demo-Frontend
ARTIFACT_ID_LOWERCASE: demo-frontend
Backend: &Backend
ARTIFACT_ID: Demo-Backend
ARTIFACT_ID_LOWERCASE: demo-backend
准备阶段
这里使用一系列脚本任务,目的是为了统一构建产物的版本,其中:
POMS=$(find . -name "pom.xml" | sort | paste -sd "," -)
是将项目当中所有依赖文件找到存到一个变量里以便后续使用,单体项目可删去
if [ "${CNB_IS_TAG}" = "true" ]; then
#判断是否为 tag_push 是则版本设置为tag和latest
TAG1=${CNB_BRANCH}
TAG2=latest
else
# 否则版本设置为 分支名-提交短哈希 (例如 dev-6bf073ba) 和 分支名 (例如 dev)
TAG1=${CNB_BRANCH}-${CNB_COMMIT_SHORT}
TAG2=${CNB_BRANCH}
fi
是设置 maven 构建出的产物的版本 $TAG1
和 docker 镜像的 tag 标签 $TAG1
和 $TAG2
后续的 printf 是根据 CNB 导出环境变量的标准把变量通过 exports 导出
可使用 printf "%s" "hello\nworld" 来输出变量,以消除标准输出流最后的换行符,同时保留 \n 等转义字符。
详细信息请看 导出环境变量
完整步骤如下:
prepare: &prepare
script: |
POMS=$(find . -name "pom.xml" | sort | paste -sd "," -)
if [ "${CNB_IS_TAG}" = "true" ]; then
TAG1=${CNB_BRANCH}
TAG2=latest
else
TAG1=${CNB_BRANCH}-${CNB_COMMIT_SHORT}
TAG2=${CNB_BRANCH}
fi
printf "##[set-output poms=%s]\n" "$POMS"
printf "##[set-output tag1=%s]\n" "$TAG1"
printf "##[set-output tag2=%s]\n" "$TAG2"
exports:
poms: POMS
tag1: TAG1
tag2: TAG2
构建缓存镜像
更多信息请看 流水线缓存 和 docker:cache
docker_cache: &docker_cache
image: maven:3.9.9-eclipse-temurin-17 # 基础镜像
type: docker:cache # 设置内置任务为 docker:cache
options:
dockerfile: ./docker/cache.Dockerfile # 用于构建缓存镜像的 Dockerfile 路径。
by: $POMS # 用来声明缓存镜像构建过程中依赖的文件列表。注意:未出现在 by 列表中的文件,除了 Dockerfile,其他在构建镜像过程中,都当不存在处理。这里本文使用准备阶段找到的pom.xml文件(s),单体项目直接写根目录pom.xml即可
exports: # 把镜像名字导出供后续使用
name: DOCKER_CACHE_IMAGE
./docker/cache.Dockerfile
文件:
注意:
- 基础镜像根据自己项目修改
- mvn 命令
-P pro
需要根据自己项目删改
# 使用带 Maven 和 JDK 17 的基础镜像
FROM maven:3.9.9-eclipse-temurin-17
# 复制项目代码到容器中
COPY . .
# 根据 COPY 过来的文件进行依赖的安装
RUN mvn -B \
--file pom.xml \
-P pro \
-DskipTests \
dependency:resolve-plugins dependency:go-offline \
assembly:help compiler:help enforcer:help exec:help failsafe:help \
install:help jar:help resources:help surefire:help \
clean:help dependency:help site:help
# 设置好需要的环境变量(本文实际尚未使用)
ENV M2_PATH=/root/.m2
构建 maven 项目
maven_package: &maven_package
image: $DOCKER_CACHE_IMAGE_NAME # 使用缓存镜像加快构建速度
volumes: # 声明数据卷
- /root/.m2:copy-on-write # 用于缓存场景,支持并发构建
script: |
mvn versions:set -DnewVersion=${TAG1} -DgenerateBackupPoms=false # 设置版本
mvn clean -B package -DskipTests # 打包(跳过测试)
构建并推送 docker 镜像
一次 build,两个标签,两次 push,其中 ${TAG1}
是每次触发的不一样的版本,${TAG2}
用于固定(分支的)最新版
--build-arg ARTIFACT_ID=${ARTIFACT_ID} --build-arg VERSION=${TAG1}
根据自己情况修改构建参数
build_push: &build_push
script: |
IMAGE_BASE=${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/${ARTIFACT_ID_LOWERCASE}
docker build -f docker/Dockerfile -t ${IMAGE_BASE}:${TAG1} -t ${IMAGE_BASE}:${TAG2} --build-arg ARTIFACT_ID=${ARTIFACT_ID} --build-arg VERSION=${TAG1} .
docker push ${IMAGE_BASE}:${TAG1}
docker push ${IMAGE_BASE}:${TAG2}
因为本文是一次性构建了所有子项目,所以后续的构建镜像、运行镜像部分所有的子项目job要并行执行节省时间,单体项目填一个即可
build_push_jobs: &build_push_jobs # 一行一个对象,并行执行,注意合并env
Frontend: { <<: *build_push, env: { <<: *Frontend } }
Backend: { <<: *build_push, env: { <<: *Backend } }
docker/Dockerfile
示例,根据项目修改,注意版本的统一,本文统一版本的操作在准备阶段
FROM eclipse-temurin:17
# 注意构建时传递参数
ARG ARTIFACT_ID
ARG VERSION
ENV VERSION=${VERSION} \
ARTIFACT_ID=${ARTIFACT_ID} \
TZ=Asia/Shanghai \
JAR_NAME=${ARTIFACT_ID}-${VERSION}.jar
# 设置时区、复制 jar 和 entrypoint 脚本并授权,全合并到一个 RUN 层中
WORKDIR /app
# 注意单体项目没有${ARTIFACT_ID}/
COPY ./${ARTIFACT_ID}/target/${JAR_NAME} /app/app.jar
COPY ./docker/entrypoint.sh /entrypoint.sh
RUN set -eux; \
apt-get update && apt-get install -y --no-install-recommends tzdata && \
ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \
dpkg-reconfigure -f noninteractive tzdata && \
chmod +x /entrypoint.sh && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/entrypoint.sh"]
docker/entrypoint.sh
示例,根据项目修改
#!/bin/bash
set -eux
exec java -Xmx2048m -Xms1024m -jar /app/app.jar "$@"
在目标环境中运行 docker 镜像
部署这里本文使用了自己制作的部署工具,详细信息在这里 ssh-deploy
也可以使用官方的ssh插件 ssh
其中小写字母的主机凭据变量要通过密钥仓库引入并配置好权限,详见 imports 权限检查
ssh_deploy: &ssh_deploy
image: docker.cnb.cool/falling42/ssh-deploy:v0.1.0
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
# 导入密钥仓库变量
settings:
use_screen: 'no'
use_jump_host: 'no'
ssh_host: ${your_ssh_host}
ssh_user: ${your_ssh_user}
ssh_private_key: ${your_ssh_private_key}
execute_remote_script: 'yes'
transfer_files: 'no'
copy_script: 'yes'
source_script: './docker/deploy.sh'
deploy_script: '/opt/ops/deploy-demo.sh'
service_name: ${ARTIFACT_ID}
service_version: "${TAG1}"
因为本文是一次性构建了所有子项目,所以后续的构建镜像、运行镜像部分所有的子项目job要并行执行节省时间,单体项目填一个即可
deploy_jobs: &deploy_jobs # 一行一个对象,并行执行,注意合并env
Frontend: { <<: *ssh_deploy, env: { <<: *Frontend } }
Backend: { <<: *ssh_deploy, env: { <<: *Backend } }
失败通知
其中小写字母的变量要通过密钥仓库引入并配置好权限,详见 imports 权限检查
#失败通知:wechat-bot
notify_wechat_bot: ¬ify_wechat_bot
image: tencentcom/wecom-message
settings:
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
robot: ${your_webhook_url}
msgType: text
content: |
🚨构 建 失 败 通 知🚨
📦仓 库: ${CNB_REPO_SLUG_LOWERCASE}
👤发 起 人: ${CNB_BUILD_USER}
🛠️失败任务: ${CNB_BUILD_FAILED_STAGE_NAME}
👉查看详情: ${CNB_BUILD_WEB_URL}
#失败通知:wechat
notify_wechat: ¬ify_wechat
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
image: clem109/drone-wechat
settings:
corpid: ${your_corpid}
corp_secret: ${your_corp_secret}
agent_id: ${your_agent_id}
to_user: ${your_user_id}
msg_url: ${CNB_BUILD_WEB_URL}
safe: 0
btn_txt: 查看详情
title: ${CNB_REPO_SLUG_LOWERCASE} 构建失败通知
description: "发起人: ${CNB_BUILD_USER}\n失败任务: ${CNB_BUILD_FAILED_STAGE_NAME}\n点击查看详情: ${CNB_BUILD_WEB_URL}\n"
# 失败通知:serverchan
notify_serverchan: ¬ify_serverchan
image: yakumioto/drone-serverchan
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
settings:
key: ${your_sct_key}
text: 🚨 构建失败通知
desp: |
> **📦 仓库:** ${CNB_REPO_SLUG_LOWERCASE}
> **👤 发起人:** ${CNB_BUILD_USER}
> **🛠️ 失败任务:** ${CNB_BUILD_FAILED_STAGE_NAME}
> **[👉 点击查看完整构建日志](${CNB_BUILD_WEB_URL})**
# 失败通知:email
notify_email: ¬ify_email
image: drillster/drone-email
imports: https://cnb.cool/<your_org>/<your_repo>/-/blob/main/yourenv.yml
settings:
host: ${your_smtp_host}$
port: 465
recipients: ${CNB_COMMITTER_EMAIL}
username: ${your_smtp_user}
password: ${your_smtp_password}
from.address: ${your_smtp_user}
from.name: CNB构建通知
subject: ${CNB_REPO_SLUG_LOWERCASE} 构建失败通知
body: |
<div style="font-family: Arial, sans-serif; padding: 12px; border: 1px solid #eee;">
<h2 style="color: #D32F2F;">🚨 构建失败通知</h2>
<p><strong>📦 仓库:</strong> ${CNB_REPO_SLUG_LOWERCASE}</p>
<p><strong>👤 发起人:</strong> ${CNB_BUILD_USER}(ID: ${CNB_BUILD_USER_ID})</p>
<p><strong>🛠️ 失败任务:</strong> ${CNB_BUILD_FAILED_STAGE_NAME}</p>
<p style="margin-top: 20px;">
👉 <a href="${CNB_BUILD_WEB_URL}" style="color: #1976D2; text-decoration: none;">
点击查看完整构建日志</a>
</p>
<hr style="margin-top: 24px;"/>
<p style="font-size: 12px; color: #888;">
来自 CNB构建通知
</p>
</div>