Skip to content

使用 CNB 构建并部署自己的项目

约 3703 字大约 12 分钟

devopscnb

2025-05-09

本文介绍了如何使用腾讯云 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/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: &notify_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: &notify_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: &notify_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: &notify_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>
贡献者: Falling42