使用 CODING 搭建前后端自动打包部署

91 阅读10分钟

背景

CODING 是腾讯云提供的云原生应用平台,提供了可视化的编排工具,支持 GitOps、CI/CD、Helm、Kubernetes 等云原生技术。笔者所在的公司使用 Jenkins 构建,由运维团队负责管理 CI/CD,效率低下,腾讯云给我们推广了这个产品,趁这个机会,笔者决定研究下 CODING。

目标

探索 CODING 流水线使用,验证 CI/CD 能力。

实战

演示工程 为例,使用 Maven 构建工具,目录结构如下。

eden-demo-cola
  |_ .coding/
    |_ Jenkinsfile # CODING 流水线脚本
    |_ settings.xml # 自定义Maven 配置文件
  |_ docker/
    |_ Dockerfile # Docker 构建文件
    |_ entrypoint.sh # Docker 容器启动脚本
  |_ ...
  |_ pom.xml # Maven 构建文件

设置 Git 代码仓库

由于笔者的项目主要托管在 Github,使用关联代码仓库完成构建。

选择 Image 构建工具

目前 Java 构建镜像的主流方式有两种:

  1. 使用 Google 的 jib-maven-plugin 插件,这种方式简单轻便,不需要在 Docker 环境下运行。
  2. 编写 Dockerfile 文件:自由度较高,结合 spring-boot-maven-plugin 插件配置分层。

Google Jib 插件构建

在子模块 eden-demo-cola-start 中引入 jib-maven-plugin 插件。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>io.github.shiyindaxiaojie.eden.demo</groupId>
		<artifactId>eden-demo-cola</artifactId>
		<version>0.0.1-SNAPSHOT</version>
		<relativePath>../pom.xml</relativePath>
	</parent>
	<artifactId>eden-demo-cola-start</artifactId>
	<packaging>jar</packaging>
	<name>eden-demo-cola-start</name>
	<description>启动入口</description>

	<properties>
		<start-class>org.ylzl.eden.demo.ColaApplication</start-class>
		<maven.deploy.skip>true</maven.deploy.skip>
		<build.layers.enabled>true</build.layers.enabled>
	</properties>

	<build>
		<finalName>${project.name}</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>com.google.cloud.tools</groupId>
				<artifactId>jib-maven-plugin</artifactId>
				<configuration>
					<from>
						<image>openjdk:11-jdk-slim</image>
					</from>
					<to>
						<image>${docker.image}</image>
						<auth>
							<username>${docker.username}</username>
							<password>${docker.password}</password>
						</auth>
						<tags>
							<tag>${project.version}</tag>
							<tag>latest</tag>
						</tags>
					</to>
					<container>
						<entrypoint>
							<shell>bash</shell>
							<option>-c</option>
							<arg>/entrypoint.sh</arg>
						</entrypoint>
						<ports>
							<port>8080</port>
							<port>9080</port>
						</ports>
						<environment>
							<TZ>Asia/Shanghai</TZ>
							<LANG>C.UTF-8</LANG>
							<JVM_XMS>1g</JVM_XMS>
							<JVM_XMX>1g</JVM_XMX>
							<JVM_XSS>256k</JVM_XSS>
							<GC_MODE>G1</GC_MODE>
							<USE_GC_LOG>Y</USE_GC_LOG>
							<USE_HEAP_DUMP>Y</USE_HEAP_DUMP>
							<USE_LARGE_PAGES>N</USE_LARGE_PAGES>
							<SPRING_PROFILES_ACTIVE>dev</SPRING_PROFILES_ACTIVE>
						</environment>
						<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
						<mainClass>${start-class}</mainClass>
					</container>
					<extraDirectories>
						<paths>src/main/docker/jib</paths>
						<permissions>
							<permission>
								<file>/entrypoint.sh</file>
								<mode>755</mode>
							</permission>
						</permissions>
					</extraDirectories>
					<allowInsecureRegistries>true</allowInsecureRegistries>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

使用 Maven 构建并发布镜像,代码片段如下:

mvn -pl eden-demo-cola-start jib:build -Dimage=shiyindaxiaojie/eden-demo-cola -Djib.disableUpdateChecks=true -DskipTests -U -T 4C

Dockerfile 分层构建

如果您不希望引入 Google 第三方插件,也可以参考 docker 目录的 Dockerfile 文件,代码实现了镜像的分层,可以放心使用。

# 使用基础镜像
FROM m.daocloud.io/openjdk:11-jdk-slim AS builder

# 指定构建模块
ARG MODULE=eden-demo-cola-start

# 设置工作目录
WORKDIR /app

# 复制必要文件
COPY $MODULE/target/$MODULE.jar application.jar
COPY docker/entrypoint.sh entrypoint.sh

# 使用 Spring Boot 的分层模式提取 JAR 文件的依赖项
RUN java -Djarmode=layertools -jar application.jar extract

# 创建容器镜像
FROM m.daocloud.io/openjdk:11-jdk-slim

# 定义元数据
LABEL maintainer="梦想歌 <shiyindaxiaojie@gmail.com>"
LABEL version="1.0.0"

# 指定构建参数
ARG USER=tmpuser
ARG GROUP=tmpgroup

# 设置环境变量
ENV HOME="/app"
ENV TZ="Asia/Shanghai"
ENV LANG="C.UTF-8"
ENV XMS="1g"
ENV XMX="1g"
ENV XSS="256k"
ENV GC_MODE="G1"
ENV USE_GC_LOG="Y"
ENV USE_HEAP_DUMP="Y"
ENV USE_LARGE_PAGES="N"
ENV SPRING_PROFILES_ACTIVE="dev"
ENV SERVER_PORT="8080"
ENV MANAGEMENT_SERVER_PORT="9080"

# 创建日志目录
RUN mkdir -p $HOME/logs \
  && touch $HOME/logs/entrypoint.out \
  && ln -sf /dev/stdout $HOME/logs/entrypoint.out \
  && ln -sf /dev/stderr $HOME/logs/entrypoint.out

# 切换工作目录
WORKDIR $HOME

# 从基础镜像复制应用程序依赖项和模块
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader ./
COPY --from=builder /app/organization-dependencies ./
COPY --from=builder /app/modules-dependencies ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
COPY --from=builder /app/entrypoint.sh ./

# 创建普通用户
RUN groupadd -g 1000 $GROUP \
  && useradd -u 1000 -g $GROUP -d $HOME -s /bin/bash $USER \
  && chown -R $USER:$GROUP $HOME \
  && chmod -R a+rwX $HOME

# 切换到容器用户
USER $USER

# 暴露访问端口
EXPOSE $SERVER_PORT $MANAGEMENT_SERVER_PORT

# 设置启动入口
CMD ["./entrypoint.sh"]

在根目录执行 Docker 指令完成构建。

docker build -f docker/Dockerfile -t shiyindaxiaojie/eden-demo-cola .
docker push shiyindaxiaojie/eden-demo-cola

自定义 Maven 配置文件

由于笔者直接使用 CODING 节点托管部署,无法了解服务器内部的 Maven 细节,并且,Maven 的环境变动可能会影响整个构建计划不可用,因此,建议自定义 settings.xml 来控制您的应用。

首先,使用 -s 选项指定项目 .coding 目录的 settings.xml 文件。

mvn package -DskipTests -T 4C -s ./.coding/settings.xml

Maven 配置文件的内容统一使用 ${env.xxx} 变量,表示通过 CODING 的脚本传递环境变量,代码片段如下。

<?xml version="1.0" encoding="UTF-8"?>
<settings>
	<servers>
		<server>
			<id>coding</id>
			<username>${env.MAVEN_USERNAME}</username>
			<password>${env.MAVEN_PASSWORD}</password>
		</server>
	</servers>
	<profiles>
		<profile>
			<id>coding</id>
			<properties>
				<docker.username>${env.DOCKER_USERNAME}</docker.username>
				<docker.password>${env.DOCKER_PASSWORD}</docker.password>
				<docker.image>${env.DOCKER_IMAGE}</docker.image>
				<altReleaseDeploymentRepository>
					coding::default::${env.MAVEN_REPO_URL}
				</altReleaseDeploymentRepository>
				<altSnapshotDeploymentRepository>
					coding::default::${env.MAVEN_REPO_URL}
				</altSnapshotDeploymentRepository>
			</properties>
			<repositories>
				<repository>
					<id>coding</id>
					<url>${env.MAVEN_REPO_URL}</url>
					<releases>
						<enabled>true</enabled>
					</releases>
					<snapshots>
						<enabled>true</enabled>
					</snapshots>
				</repository>
			</repositories>
			<pluginRepositories>
				<pluginRepository>
					<id>coding</id>
					<url>${env.MAVEN_REPO_URL}</url>
					<releases>
						<enabled>true</enabled>
					</releases>
					<snapshots>
						<enabled>false</enabled>
					</snapshots>
				</pluginRepository>
			</pluginRepositories>
		</profile>
	</profiles>
	<activeProfiles>
		<activeProfile>coding</activeProfile>
	</activeProfiles>
</settings>

对应的 CODING 流水线传递相关变量到 settings.xml

stage('推送到 Maven 制品库') {
  steps {
    withCredentials([
      usernamePassword(
        credentialsId: env.MAVEN_RELEASES,
        usernameVariable: 'MAVEN_RELEASES_USERNAME',
        passwordVariable: 'MAVEN_RELEASES_PASSWORD'
      ),
      usernamePassword(
        credentialsId: env.MAVEN_SNAPSHOTS,
        usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
        passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
      )
    ]) {
      withEnv([
        "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
        "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
        "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
        "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
        "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
        "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
        "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
        "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
      ]) {
        sh 'mvn -T 4C -Pcoding deploy -DskipTests -s ./.coding/settings.xml'
      }
    }
  }
}

设置 Docker 和 Maven 凭据

使用项目凭据来替换 CODING 脚本所需的 Docker/Maven 账户信息,减少维护凭据的工作,允许多个构建计划复用同一个凭据。同时防止构建计划泄露账户信息,特别是多人协作部署。

录入凭据后,在构建计划的 变量与缓存 选择相关凭据。

编排 Jenkinsfile 脚本

由于 CODING 目前没有实现 Jenkinsfile 脚本的版本控制,为防止误删配置导致整个构建计划不可用,笔者的做法是把 Jenkinsfile 脚本保存到工程 .coding 目录下,使用 Git 完成版本控制。

.coding 目录提供了开箱即用的 Jenkinsfile,包含编译打包、单元测试、发布私服、推送镜像等步骤,完整代码如下。

pipeline {
  agent any
	environment {
	  MAVEN_SNAPSHOTS_NAME = "maven-snapshots"
		MAVEN_SNAPSHOTS_ID = "${CCI_CURRENT_TEAM}-${PROJECT_NAME}-${MAVEN_SNAPSHOTS_NAME}"
    MAVEN_SNAPSHOTS_URL = "${CCI_CURRENT_WEB_PROTOCOL}://${CCI_CURRENT_TEAM}-maven.pkg.${CCI_CURRENT_DOMAIN}/repository/${PROJECT_NAME}/${MAVEN_SNAPSHOTS_NAME}/"

	  MAVEN_RELEASES_NAME = "maven-releases"
		MAVEN_RELEASES_ID = "${CCI_CURRENT_TEAM}-${PROJECT_NAME}-${MAVEN_RELEASES_NAME}"
    MAVEN_RELEASES_URL = "${CCI_CURRENT_WEB_PROTOCOL}://${CCI_CURRENT_TEAM}-maven.pkg.${CCI_CURRENT_DOMAIN}/repository/${PROJECT_NAME}/${MAVEN_RELEASES_NAME}/"

	  MAVEN_SNAPSHOTS_NAME = "maven-snapshots"
	  MAVEN_RELEASES_NAME = "maven-releases"
	  DOCKER_REPOSITORY_NAME = "docker"

		MAVEN_SNAPSHOTS_ID = "${CCI_CURRENT_TEAM}-${PROJECT_NAME}-${MAVEN_SNAPSHOTS_NAME}"
		MAVEN_SNAPSHOTS_URL = "${CCI_CURRENT_WEB_PROTOCOL}://${CCI_CURRENT_TEAM}-maven.pkg.${CCI_CURRENT_DOMAIN}/repository/${PROJECT_NAME}/${MAVEN_SNAPSHOTS_NAME}/"
		MAVEN_RELEASES_ID = "${CCI_CURRENT_TEAM}-${PROJECT_NAME}-${MAVEN_RELEASES_NAME}"
		MAVEN_RELEASES_URL = "${CCI_CURRENT_WEB_PROTOCOL}://${CCI_CURRENT_TEAM}-maven.pkg.${CCI_CURRENT_DOMAIN}/repository/${PROJECT_NAME}/${MAVEN_RELEASES_NAME}/"
	  DOCKER_REPOSITORY = "${CCI_CURRENT_TEAM}-docker.pkg.${CCI_CURRENT_DOMAIN}/${PROJECT_NAME}/${DOCKER_REPOSITORY_NAME}"
	}

  stages {
    stage('检出') {
      steps {
        checkout([$class: 'GitSCM',
        branches: [[name: GIT_BUILD_REF]],
        userRemoteConfigs: [[
          url: GIT_REPO_URL,
          credentialsId: CREDENTIALS_ID
        ]]])
      }
    }

    stage('编译') {
      steps {
        script {
          if (env.TAG_NAME ==~ /.*/ ) {
	          ARTIFACT_VERSION = "${env.TAG_NAME}"
          } else if (env.MR_SOURCE_BRANCH ==~ /.*/ ) {
	          ARTIFACT_VERSION = "${env.MR_RESOURCE_ID}-${env.GIT_COMMIT_SHORT}"
          } else {
	          ARTIFACT_VERSION = "${env.BRANCH_NAME.replace('/', '-')}-${env.GIT_COMMIT_SHORT}"
          }
        }
        withCredentials([
            usernamePassword(
                credentialsId: env.MAVEN_RELEASES,
                usernameVariable: 'MAVEN_RELEASES_USERNAME',
                passwordVariable: 'MAVEN_RELEASES_PASSWORD'
            ),
            usernamePassword(
                credentialsId: env.MAVEN_SNAPSHOTS,
                usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
                passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
            )
        ]) {
            withEnv([
                "ARTIFACT_VERSION=${ARTIFACT_VERSION}",
                "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
                "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
                "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
                "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
                "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
                "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
                "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
                "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
            ]) {
                sh 'mvn -T 4C -U -Pcoding versions:set -DnewVersion=${ARTIFACT_VERSION} package -DskipTests -s ./.coding/settings.xml'
            }
        }
      }
    }

    stage('单元测试') {
        steps {
            withCredentials([
                usernamePassword(
                    credentialsId: env.MAVEN_RELEASES,
                    usernameVariable: 'MAVEN_RELEASES_USERNAME',
                    passwordVariable: 'MAVEN_RELEASES_PASSWORD'
                ),
                usernamePassword(
                    credentialsId: env.MAVEN_SNAPSHOTS,
                    usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
                    passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
                )
            ]) {
                withEnv([
                    "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
                    "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
                    "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
                    "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
                    "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
                    "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
                    "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
                    "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
                ]) {
                    sh 'mvn -T 4C -Pcoding,unit-test test -s ./.coding/settings.xml'
                }
            }
        }
        post {
            always {
                junit '**/surefire-reports/*.xml'
                codingHtmlReport(name: 'eden-demo-cola-adapter-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-adapter/target/site/jacoco', entryFile: 'index.html')
                codingHtmlReport(name: 'eden-demo-cola-app-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-app/target/site/jacoco', entryFile: 'index.html')
                codingHtmlReport(name: 'eden-demo-cola-infrastructure-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-infrastructure/target/site/jacoco', entryFile: 'index.html')
            }
		}
    }

    stage('推送到 Maven 制品库') {
      steps {
        withCredentials([
            usernamePassword(
                credentialsId: env.MAVEN_RELEASES,
                usernameVariable: 'MAVEN_RELEASES_USERNAME',
                passwordVariable: 'MAVEN_RELEASES_PASSWORD'
            ),
            usernamePassword(
                credentialsId: env.MAVEN_SNAPSHOTS,
                usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
                passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
            )
        ]) {
            withEnv([
                "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
                "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
                "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
                "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
                "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
                "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
                "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
                "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
            ]) {
                sh 'mvn -T 4C -Pcoding deploy -DskipTests -s ./.coding/settings.xml'
            }
        }
      }
    }

    stage('推送到 Docker 制品库') {
        steps {
            withCredentials([
                usernamePassword(
                    credentialsId: env.DOCKER_REGISTRY_CREDENTIALS_ID,
                    usernameVariable: 'DOCKER_USERNAME',
                    passwordVariable: 'DOCKER_PASSWORD'
                )
            ]) {
                withEnv([
                    "DOCKER_USERNAME=${DOCKER_USERNAME}",
        "DOCKER_PASSWORD=${DOCKER_PASSWORD}"
            ]) {
                sh "docker login ${DOCKER_REPOSITORY} -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD}"
                sh "docker build -t ${DOCKER_REPOSITORY}/${DEPOT_NAME}:${ARTIFACT_VERSION} -f docker/Dockerfile ."
                sh "docker push ${DOCKER_REPOSITORY}/${DEPOT_NAME}:${ARTIFACT_VERSION}"
                sh "docker push ${DOCKER_REPOSITORY}/${DEPOT_NAME}:latest"
            }
        }
        }
    }
  }
}

如果您不熟悉 Jenkinsfile 的语法,可以复制上述代码,切换到 CODING 图形化视角,进入可视化模式调整。

编辑好 Jenkinsfile 文件后保存,点击 立即构建,触发流水线即可。接下来,笔者根据 Jenkinsfile 内容中的关键代码块进行讲解。

自定义检出 Git 代码

正常情况下,我们是一对一检出 Git 仓库。

stage('检出') {
  steps {
    checkout([$class: 'GitSCM',
    branches: [[name: GIT_BUILD_REF]],
    userRemoteConfigs: [[
      url: GIT_REPO_URL,
      credentialsId: CREDENTIALS_ID
    ]]])
  }
}

有些特殊场景,需要同时检出多个 Git 仓库,详见下述代码片段。不过,这样做会导致您无法有效控制 Git 自动触发。

stage('检出') {
  parallel { // 开启并行构建
    stage('检出 eden-gateway') {
      steps {
        dir('eden-gateway') {
          checkout([$class: 'GitSCM',
          branches: [[name: env.EDEN_GATEWAY_VERSION]],
          userRemoteConfigs: [[
            url: env.EDEN_GATEWAY_GIT_URL,
            credentialsId: CREDENTIALS_ID
          ]]])
          script {
            // ...
          }
        }
      }
    }

    stage('检出 eden-uaa') {
      steps {
        dir('eden-uaa') {
          checkout([$class: 'GitSCM',
          branches: [[name: env.EDEN_UAA_VERSION]],
          userRemoteConfigs: [[
            url: env.EDEN_UAA_GIT_URL,
            credentialsId: CREDENTIALS_ID
          ]]])
          script {
            // ...
          }
        }
      }
    } 
  }  
}                                                                 

设置 Maven 魔法版本号

CODING 内置了一系列丰富的环境变量,您可以通过 ${env.TAG_NAME} 或者 ${env.BRANCH_NAME} 获取 Git 的版本号,使用 versions-maven-plugin 执行 mvn versions:set -DnewVersion=${VERSION} 动态设置 Maven 模块的版本号。

stage('编译') {
  steps {
    script {
      if (env.TAG_NAME ==~ /.*/ ) {
        ARTIFACT_VERSION = "${env.TAG_NAME}"
      } else if (env.MR_SOURCE_BRANCH ==~ /.*/ ) {
        ARTIFACT_VERSION = "${env.MR_RESOURCE_ID}-${env.GIT_COMMIT_SHORT}"
      } else {
        ARTIFACT_VERSION = "${env.BRANCH_NAME.replace('/', '-')}-${env.GIT_COMMIT_SHORT}"
      }
    }
    withCredentials([
      usernamePassword(
        credentialsId: env.MAVEN_RELEASES,
        usernameVariable: 'MAVEN_RELEASES_USERNAME',
        passwordVariable: 'MAVEN_RELEASES_PASSWORD'
      ),
      usernamePassword(
        credentialsId: env.MAVEN_SNAPSHOTS,
        usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
        passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
      )
    ]) {
      withEnv([
        "ARTIFACT_VERSION=${ARTIFACT_VERSION}",
        "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
        "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
        "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
        "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
        "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
        "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
        "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
        "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
      ]) {
        sh 'mvn -T 4C -U -Pcoding versions:set -DnewVersion=${ARTIFACT_VERSION} package -DskipTests -s ./.coding/settings.xml'
      }
    }
  }
}

Java 测试报告可视化

CODING 提供了测试报告的可视化支持,下述代码片段提供了单元测试代码覆盖率分析的配置示例。

stage('单元测试') {
  steps {
    withCredentials([
      usernamePassword(
        credentialsId: env.MAVEN_RELEASES,
        usernameVariable: 'MAVEN_RELEASES_USERNAME',
        passwordVariable: 'MAVEN_RELEASES_PASSWORD'
      ),
      usernamePassword(
        credentialsId: env.MAVEN_SNAPSHOTS,
        usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
        passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
      )
    ]) {
      withEnv([
        "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
        "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
        "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
        "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
        "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
        "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
        "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
        "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
      ]) {
        sh 'mvn -T 4C -Pcoding,unit-test test -s ./.coding/settings.xml'
      }
    }
  }
  post {
    always {
      junit '**/surefire-reports/*.xml' // 单元测试报告
      codingHtmlReport(name: 'eden-demo-cola-adapter-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-adapter/target/site/jacoco', entryFile: 'index.html')
      codingHtmlReport(name: 'eden-demo-cola-app-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-app/target/site/jacoco', entryFile: 'index.html')
      codingHtmlReport(name: 'eden-demo-cola-infrastructure-jacoco-reports', tag: '代码覆盖率报告', path: 'eden-demo-cola-infrastructure/target/site/jacoco', entryFile: 'index.html')
    }
  }
}

在构建记录中,点击 测试报告 页签,查看单元测试报告。

通用报告 查看代码覆盖率报告。

发布制品到 Maven 仓库

多人协同下,需要把构建生成的 API 依赖发布到私服(制品库)。在此之前我们已经配置好了 Maven 私服地址和相关凭据,具体可以参考下面的代码片段。

stage('推送到 Maven 制品库') {
  steps {
    withCredentials([
      usernamePassword(
        credentialsId: env.MAVEN_RELEASES,
        usernameVariable: 'MAVEN_RELEASES_USERNAME',
        passwordVariable: 'MAVEN_RELEASES_PASSWORD'
      ),
      usernamePassword(
        credentialsId: env.MAVEN_SNAPSHOTS,
        usernameVariable: 'MAVEN_SNAPSHOTS_USERNAME',
        passwordVariable: 'MAVEN_SNAPSHOTS_PASSWORD'
      )
    ]) {
      withEnv([
        "MAVEN_RELEASES_ID=${MAVEN_RELEASES_ID}",
        "MAVEN_RELEASES_URL=${MAVEN_RELEASES_URL}",
        "MAVEN_RELEASES_USERNAME=${MAVEN_RELEASES_USERNAME}",
        "MAVEN_RELEASES_PASSWORD=${MAVEN_RELEASES_PASSWORD}",
        "MAVEN_SNAPSHOTS_ID=${MAVEN_SNAPSHOTS_ID}",
        "MAVEN_SNAPSHOTS_URL=${MAVEN_SNAPSHOTS_URL}",
        "MAVEN_SNAPSHOTS_USERNAME=${MAVEN_SNAPSHOTS_USERNAME}",
        "MAVEN_SNAPSHOTS_PASSWORD=${MAVEN_SNAPSHOTS_PASSWORD}"
      ]) {
        sh 'mvn -T 4C -Pcoding deploy -DskipTests -s ./.coding/settings.xml'
      }
    }
  }
}

Maven 私服应当严格区分 ReleasesSnapshots 仓库。

在开发测试阶段,需要利用 Maven 的快照更新维持 API 的变动,使用 Snapshots 仓库比较合适。

在预发布阶段,表示测试已通过,可以发布生产了,API 不允许有变动,应当严格使用 Releases 仓库来限制版本的发布,避免相同版本的 API 被覆盖。

在 CODING 制品库,您可以在 Releases 仓库设置版本策略为禁止覆盖版本。

推送镜像到 Docker 仓库

本文中,笔者使用 Docker Hub 托管镜像,代码中的变量,在设置凭据的小节已详细说明,您可以根据实际情况进行调整。

stage('推送到 Docker 制品库') {
  steps {
    withCredentials([
      usernamePassword(
        credentialsId: env.DOCKER_CREDENTIALS,
        usernameVariable: 'DOCKER_USERNAME',
        passwordVariable: 'DOCKER_PASSWORD'
      )
    ]) {
      withEnv([
        "DOCKER_USERNAME=${DOCKER_USERNAME}",
        "DOCKER_PASSWORD=${DOCKER_PASSWORD}",
        "DOCKER_IMAGE=${DOCKER_REPOSITORY}:${ARTIFACT_VERSION}"
      ]) {
        sh 'mvn -Pcoding -pl eden-demo-cola-start jib:build -Djib.disableUpdateChecks=true -DskipTests -s ./.coding/settings.xml'
      }
    }
  }
}

执行上述代码后,在 DockerHub 查看镜像已更新,验证通过。

更新镜像到 K8s 集群

CODING 支持配置镜像更新到 K8s 已部署的工作负载。

stage('部署到 K8s 集群') {
  steps {
    withEnv(["DOCKER_IMAGE=${DOCKER_REPOSITORY}:${ARTIFACT_VERSION}"]) {
      cdDeploy(deployType: 'PATCH_IMAGE', application: '${CCI_CURRENT_TEAM}', pipelineName: '${PROJECT_NAME}-${CCI_JOB_NAME}-5001969', image: '${DOCKER_IMAGE}', cloudAccountName: 'test', namespace: 'test', manifestType: 'Deployment', manifestName: 'demo-cola', containerName: 'demo-cola', credentialId: 'a2954a785e1d40caa1803274a23edac9', personalAccessToken: '${CD_PERSONAL_ACCESS_TOKEN}')
    }
  }
}

在 CODING 的 持续部署 > 弹性伸缩 > 配置云账号 添加 K8s 凭据,绑定需要部署的 K8s 集群。

在 CODING 构建计划编排界面配置 镜像更新,选择上述已绑定的 K8s 集群,根据级联选择到具体的 Pod 容器。

点击构建,查看镜像更新执行记录。

除了控制台输出,也可以点击查看 K8s 发布详情。

查看 K8s 集群(笔者使用腾讯云 Serveless 集群部署),启动成功!

访问控制台输出的 External Access URL 地址,系统访问正常。

前端项目怎么构建?

以开源的若依项目(Vue)为例,在代码目录下分别创建以下目录:

eden-ui
  |_ .coding/
    |_ Jenkinsfile # CODING 流水线脚本
    |_ nginx.conf # Nginx 配置文件
  |_ docker/
    |_ Dockerfile # Docker 构建文件
  |_ ...
  |_ .npmrc

初始化 .npmrc,代理 npm 源,指向 CODING 制品库。

registry=https://xxx-npm.pkg.coding.net/xxx/npm/
always-auth=true
//xxx-npm.pkg.coding.net/xxx/npm/:username=${CODING_ARTIFACTS_USERNAME}
//xxx-npm.pkg.coding.net/xxx/npm/:_password=${CODING_ARTIFACTS_PASSWORD}
//xxx-npm.pkg.coding.net/xxx/npm/:email=xxx@gmail.com

为了让前端单独运行,需要依赖 Nginx 的镜像进行部署,所以,我们要把 nginx 的配置也加上,nginx.conf 配置如下。

user  nginx;
worker_processes  1;

error_log  logs/error.log warn;
pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    client_max_body_size   500m;

    server {
        listen       80;
        server_name  localhost;
        charset utf-8;

        gzip_static on;
        gzip_vary on;
        gzip_min_length 1k;
        gzip_comp_level 9;
        gzip_types text/css text/javascript application/javascript application/x-javascript application/xml;
        gzip_disable "MSIE [1-6]\.";

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

        location / { # 前端
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location ^~ /api/  { # 后端
            proxy_pass http://xxx:8080/; 
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

调整 Dockerfile 打包脚本,指定 Vue 项目打包后的静态资源到 Nginx 的静态资源目录中。

FROM nginx:1.15.2-alpine

LABEL maintainer="梦想歌"

COPY dist/ /usr/share/nginx/html

COPY .coding/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

编写 Jenkinsfile 构建脚本,参考如下内容。

pipeline {
    agent any
    environment {
        CODING_DOCKER_REPO_NAME = "docker"
        CODING_DOCKER_REPO_HOST = "${CCI_CURRENT_TEAM}-docker.pkg.${CCI_CURRENT_DOMAIN}"
        CODING_DOCKER_REPO_URL = "${CODING_DOCKER_REPO_HOST}/${PROJECT_NAME}/${CODING_DOCKER_REPO_NAME}/${DEPOT_NAME}"

        TCR_NAMESPACE_NAME = "xxx"
        TCR_DOCKER_REPO_URL = "shjrccr.ccs.tencentyun.com/${TCR_NAMESPACE_NAME}/${DEPOT_NAME}"
    }
    stages {
        stage('检出') {
            steps {
                checkout([$class: 'GitSCM',
                branches: [[name: GIT_BUILD_REF]],
                userRemoteConfigs: [[
                url: GIT_REPO_URL,
                credentialsId: CREDENTIALS_ID
                ]]])
            }
        }
        stage('编译') {
            steps {
                script {
                    if (env.TAG_NAME ==~ /.*/ ) {
                        CODING_ARTIFACT_VERSION = "${env.TAG_NAME}"
                    } else if (env.MR_SOURCE_BRANCH ==~ /.*/ ) {
                        CODING_ARTIFACT_VERSION = "mr-${env.MR_RESOURCE_ID}-${env.GIT_COMMIT_SHORT}"
                    } else {
                        CODING_ARTIFACT_VERSION = "${env.BRANCH_NAME.replace('/', '-')}-${env.GIT_COMMIT_SHORT}"
                    }
                }
                sh 'rm -rf /usr/lib/node_modules/npm/'
                dir ('/root/.cache/downloads') {
                    sh 'wget -nc "https://coding-public-generic.pkg.coding.net/public/downloads/node-linux-x64.tar.xz?version=v16.13.0" -O node-v16.13.0-linux-x64.tar.xz | true'
                    sh 'tar -xf node-v16.13.0-linux-x64.tar.xz -C /usr --strip-components 1'
                }
                withCredentials([
                    usernamePassword(
                        credentialsId: env.CODING_ARTIFACTS_CREDENTIALS_ID,
                        usernameVariable: 'CODING_ARTIFACTS_USERNAME',
                        passwordVariable: 'CODING_ARTIFACTS_PASSWORD'
                    )]) {
                    script {
                        sh '''
                        echo "CODING_ARTIFACTS_USERNAME=${CODING_ARTIFACTS_USERNAME}" >> $CI_ENV_FILE
                        echo "CODING_ARTIFACTS_PASSWORD=${CODING_ARTIFACTS_PASSWORD}" >> $CI_ENV_FILE
                        '''
                        readProperties(file: env.CI_ENV_FILE).each {
                            key, value -> env[key] = value 
                        }
                    }
                    sh 'npm install'
                    sh 'npm run build:prod'
                }
            }
        }
        stage('推送到 Docker 制品库') {
            steps {
                withEnv([
                    "DOCKER_USERNAME=${DOCKER_USERNAME}",
                    "DOCKER_PASSWORD=${DOCKER_PASSWORD}",
                    "DOCKER_IMAGE=${TCR_DOCKER_REPO_URL}:${CODING_ARTIFACT_VERSION}"
                ]) {
                    sh 'docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PASSWORD} shjrccr.ccs.tencentyun.com'
                    sh 'docker build -t ${DOCKER_IMAGE} .'
                    sh 'docker tag ${DOCKER_IMAGE} ${DOCKER_IMAGE}'
                    sh 'docker push ${DOCKER_IMAGE}'
                }
            }
        }
        stage('部署到 K8s 集群') {
            steps {
                withEnv(["DOCKER_IMAGE=${TCR_DOCKER_REPO_URL}:${CODING_ARTIFACT_VERSION}"]) {
                    cdDeploy(deployType: 'PATCH_IMAGE', application: '${CCI_CURRENT_TEAM}', pipelineName: '${PROJECT_NAME}-${CCI_JOB_NAME}-202222222', image: '${DOCKER_IMAGE}', cloudAccountName: 'xxx-k8s-test', namespace: 'test', manifestType: 'Deployment', manifestName: 'xxx-client', containerName: 'xxx-client', credentialId: 'aaaaaaaaaaaaaaaaaaaaaaa', personalAccessToken: '${CD_PERSONAL_ACCESS_TOKEN}')
                }
            }
        }
    }
}

构建成功的效果图如下。

总结

在公司引入 CODING 后,研发团队和运维团队的工作都发生了很大的变化。研发团队负责 DevOps 的全生命周期管理,不再依赖运维团队,效率提升明显。而运维团队专注于基础设施,如云原生、网络、操作系统的优化,能更有效地处理线上故障。从整体的收益来看,至少节省了两个员工的成本。

CODING 流水线存在一些不足,只提供了单一镜像的更新功能,只能用于已部署的目标,并且不适合多服务批量部署,除非手动写脚本,基于 kubectl apply 实现。和腾讯方沟通后,他们告诉笔者,目前有个 Orbit 云原生应用交付正在内测,即将上线,可以开放白名单给我们尝试下。

下一期,笔者将介绍 CODING Orbit 云原生应用交付。