第39讲:使用 Jenkins 进行持续集成

从本课时开始,我们将开始进入持续集成(Continuous Integration)持续部署(Continuous Deployment) 相关的内容,持续集成和部署是目前软件开发中的标准实践。在微服务架构的应用中,持续集成和部署的重要性和复杂度都提高了,因为每个服务都需要独立的集成和部署。本课时将介绍如何使用 Jenkins 进行持续集成。

我们首先要明确的是持续集成的目标,即从源代码到容器镜像,每一个源代码的提交,都应该创建出对应的不可变的容器镜像,这个构建过程是可重复的。对于同样的代码提交,无论在什么时候构建,所得到的容器镜像都应该是完全相同的,这就保证了容器镜像是可丢弃的,可以随时从源代码中构建出所需要的镜像,这使得我们可以把不同环境上的应用部署回退到任意版本。创建出来的镜像一般被发布到镜像注册表中,由 Kubernetes 在运行时拉取并运行。

下面首先介绍如何使用 Dockerfile 创建镜像。

使用 Dockerfile

为了部署在 Kubernetes 上,我们需要为每个微服务创建各自的容器镜像。

创建简单的 Docker 镜像并不是复杂的事情,只需要编写描述镜像内容的 Dockerfile 文件,再使用 docker build 命令来创建镜像即可。下面代码中的 Dockerfile 用来创建地址管理服务的镜像。

FROM adoptopenjdk/openjdk8:jre8u262-b10-alpine 
ADD target/happyride-address-service-1.0.0-SNAPSHOT.jar /opt/app.jar 
ENTRYPOINT [ "java", "-jar", "/opt/app.jar" ]

这个 Dockerfile 的内容很简单,只有 3 条指令,具体的说明如下表所示。

指令说明
FROM使用的基础镜像是 AdoptOpenJDK 的 JRE 8 的 Alpine 镜像
ADD添加服务的 JAR 文件到 /opt 目录
ENTRYPOINT设置容器镜像运行时的入口为使用 Java 命令来运行 JAR 文件

这里利用了 Spring Boot 的 Maven 插件来把整个应用打包成单一的 JAR 文件。

我们使用下面的命令来创建并运行镜像,-t 参数的作用是为镜像指定一个标签:

$ docker build . -t local/address-service:1.0.0 
$ docker run local/address-service:1.0.0

在 Maven 构建过程的 package 阶段中,在 Spring Boot 的 Maven 插件产生了 JAR 文件之后,使用 Maven 的 exec-maven-plugin 插件来调用 docker build 命令。下面的代码给出了 Maven 插件的使用示例。

<plugin> 
  <groupId>org.codehaus.mojo</groupId> 
  <artifactId>exec-maven-plugin</artifactId> 
  <version>3.0.0</version> 
  <executions> 
    <execution> 
      <phase>package</phase> 
      <goals> 
        <goal>exec</goal> 
      </goals> 
    </execution> 
  </executions> 
  <configuration> 
    <executable>docker</executable> 
    <arguments> 
      <argument>build</argument> 
      <argument>.</argument> 
      <argument>-f</argument> 
      <argument>${project.basedir}/src/docker/Dockerfile</argument> 
      <argument>-t</argument> 
      <argument>happyride/${project.artifactId}:${project.version} 
      </argument> 
    </arguments> 
  </configuration> 
</plugin>

在使用 Maven 插件完成构建之后,产生的容器镜像会缓存在本地,可以通过 docker images 命令来查看。

OCI 镜像规范

虽然我们在谈论容器镜像时,通常会使用 Docker 镜像来代替,但两者并不是等同的。为了对容器镜像的格式进行规范化,Linux 基金会下的开放容器倡议(Open Container Initiative,OCI)组织负责维护容器镜像规范和容器运行规范。在 OCI 规范的基础上,不同的厂商可以开发自己的基于 OCI 规范的工具或产品。

OCI 镜像规范以 Docker 公司贡献的 Docker 镜像版本 2 格式作为基础。每个 OCI 镜像由下表中的几个部分组成:

组成部分说明
清单文件描述对应于特定底层架构和操作系统的容器镜像
清单文件索引清单文件的索引
层(Layer)对文件系统的改动
配置与容器运行相关的配置

在上表中,镜像中的层是开发中需要注意的概念。把镜像划分成多个层之后,可以更有效地利用缓存,从而加快构建的速度。在推送镜像到注册表时,只有改变的层才会被推送。以一个 Java 应用来说,如果把应用所依赖的第三方库和应用自身的类文件划分成不同的层,由于第三方库很少变化,在构建镜像时,只需要更新和推送类文件所在的层即可。如果整个 Java 应用的全部文件被划分在一个层中,那么每次构建镜像时,该层都必然被更新,即便其中的第三方库没有变化,这也会产生不必要的传输开销。

使用 Spring Boot 的 Buildpacks

Dockerfile 虽然简单易懂,但是缺乏必要的组织,造成复用起来很困难,只能复制粘贴 Dockerfile 中的部分内容。如果有很多服务都使用 Spring Boot 开发,那么每个服务中都需要复制一份大部分内容都重复的 Dockerfile。解决这个问题的一种做法是使用 Buildpacks。

Buildpacks是 CNCF 之下的一个沙盒项目,其所要解决的问题是从源代码中构建出 OCI 容器镜像。与 Dockerfile 相比,Buildpacks 的抽象层次更高,更容易理解和复用,它的基本组成单元是 Buildpack。每个 Buildpack 分成检测构建两个步骤:检测步骤用来判断该 Buildpack 是否应该被应用;构建步骤则负责对镜像进行修改,包括修改层的内容,或是修改配置。每个 Buildpack 只对镜像做特定的改动。

在每一个应用的镜像构建过程中,会有多个 Buildpack 按照顺序来依次检测和应用修改。通过这种方式,不同的 Buildpack 可以进行组合和复用。

Spring Boot 从 2.3.0 版本开始,支持使用 Buildpacks 来创建 OCI 镜像。在 Maven 中,只需要使用 Spring Boot Maven 插件的 build-image 命令即可。下面的代码给出了插件的基本用法。

<plugin> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-maven-plugin</artifactId> 
  <executions> 
    <execution> 
      <goals> 
        <goal>build-image</goal> 
      </goals> 
    </execution> 
  </executions> 
</plugin>

在生成的 Spring Boot 镜像中,不仅仅包含了 JRE 和应用的 JAR 文件,还包含了一些辅助的工具,如下表所示:

工具说明
memory-calculator计算 JVM 使用的内存大小
jvmkill当无法分配内存或创建线程时,终止 JVM
class-counter计算类文件的数量
link-local-dns修改 DNS 设置
openssl-security-provider加载 JRE 的权威机构证书
security-providers-configurer配置 Java 的安全服务的提供者
java-security-propertiesJava 安全属性

这些工具的作用是优化 Java 应用在容器中的运行。下面的代码是 Spring Boot 应用的容器在运行时的输出,从中可以看到,JVM 的内存设置会根据容器的内存限制来做出调整。

Container memory limit unset. Configuring JVM for 1G container. 
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=126109K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx410466K (Head Room: 0%, Loaded Class Count: 19851, Thread Count: 250, Total Memory: 1073741824) 
Adding 127 container CA certificates to JVM truststore

下面的代码给出了 Spring Boot 的 Buildpacks 在构建时的部分输出,从中可以看到,在检测阶段中,在 16 个 Buildpack 中, 只有 5 个会参与构建,并给出了每个 Buildpack 的名称和版本号。

[INFO]  > Running creator 
[INFO]     [creator]     ===> DETECTING 
[INFO]     [creator]     5 of 16 buildpacks participating 
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 2.11.0 
[INFO]     [creator]     paketo-buildpacks/executable-jar    2.0.2 
[INFO]     [creator]     paketo-buildpacks/apache-tomcat     1.4.0 
[INFO]     [creator]     paketo-buildpacks/dist-zip          1.3.8 
[INFO]     [creator]     paketo-buildpacks/spring-boot       2.3.0

使用 Jib

Jib 是由 Google 维护的工具,用来对 Java 应用进行容器化。其优势在于不需要依赖 Docker 守护进程,就可以创建出 OCI 容器镜像,构建镜像也不需要编写 Dockerfile。Jib 会自动把应用分成多层,把应用依赖的第三方库和应用自身的类文件分开。

Jib 支持 3 种不同的构建方式,对应于不同的 Maven 目标,如下表所示。

Maven 目标说明
build不使用 Docker 来构建镜像,并推送到注册表
dockerBuild使用 Docker 来构建镜像
buildTar把镜像打包成 tar 文件

下面的代码给出了 jib 的 Maven 插件的基本用法,其中 from 表示基础镜像的名称,to 表示构建出来的镜像的发布地址。这里使用的是 build 目标来创建并发布 OCI 镜像,并不依赖 Docker。

<plugin> 
  <groupId>com.google.cloud.tools</groupId> 
  <artifactId>jib-maven-plugin</artifactId> 
  <version>2.4.0</version> 
  <configuration> 
    <from> 
      <image>adoptopenjdk/openjdk8:jre8u262-b10-alpine</image> 
    </from> 
    <to> 
      <image> 
        docker-registry:5000/happyride/${project.artifactId}:${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}-${git.commit.id.abbrev} 
      </image> 
    </to> 
    <allowInsecureRegistries>true</allowInsecureRegistries> 
    <container> 
      <format>OCI</format> 
    </container> 
  </configuration> 
  <executions> 
    <execution> 
      <phase>package</phase> 
      <goals> 
        <goal>build</goal> 
      </goals> 
    </execution> 
  </executions> 
</plugin>

与 Spring Boot 构建镜像的方式相比,Jib 的优势在于可以对任意 Java 应用进行构建,并且不依赖 Docker 运行时的支持;不足之处在于缺少对 Java 应用运行的优化。

容器镜像注册表

在本地开发环境上运行 docker build 命令之后,产生的镜像会缓存在本地。当在 Kubernetes 上部署时,这些本地缓存的镜像并不能直接使用。根据部署环境的不同,可以通过相应的方式来使用镜像。

在本地开发环境中,Minikube 内置包含了 Docker 运行时,为 Kubernetes 提供容器运行时的支持。我们可以配置本地开发环境中的 docker 命令,来连接 Minikube 中的 Docker 守护进程。在本地上构建完成之后,得到的镜像会被缓存在 Minikube 的 Docker 进程中,从而可以在 Kubernetes 中直接使用。

使用下面的命令可以显示连接到 Minikube 的 Docker 守护进程的配置方式。

$ minikube docker-env

上述命令的输出如下所示,只是设置了一些环境变量:

export DOCKER_TLS_VERIFY="1" 
export DOCKER_HOST="tcp://192.168.64.9:2376" 
export DOCKER_CERT_PATH="/Users/alexcheng/.minikube/certs" 
export MINIKUBE_ACTIVE_DOCKERD="minikube" 
# To point your shell to minikube's docker-daemon, run: 
# eval $(minikube -p minikube docker-env)

按照命令输出中的提示操作完成设置之后,在当前命令行窗口中使用 Maven 命令来构建镜像。在 Kubernetes 中,只需要重新创建 Pod,就可以使用新构建的镜像来进行测试。

如果应用安装在用户的私有环境中,并且不能自由的访问外部的网络,可以使用 docker export 命令把镜像导出成压缩文件,保存在移动存储设备中。在内部网络中,把镜像的压缩文件复制到 Kubernetes 的每个节点上,再使用 docker import 命令把镜像导入缓存中。

除了上述两种特殊情况之外,最常用的做法是使用容器镜像注册表来保存镜像。在持续集成中,容器镜像被发布到注册表中;在 Kubernetes 上运行时,容器镜像从注册表中下载到本地并运行。

云平台一般都提供各自的容器注册表服务,也可以使用独立的注册表服务。Docker 的注册表实现是开源的,可以在集群内部安装自己的私有注册表。

容器镜像的标签

在持续集成中,每次构建出来的容器镜像都应该有唯一的标签,如果不指定标签,那么默认使用的是 latest 标签。在实际的开发中,并不建议使用 latest 标签,因为该标签所指向的容器镜像的内容是不固定的。如果在测试或生产环境中使用了 latest 标签,那么一段时间之后重新运行测试或再次部署时,所得到的结果可能完全不同,因为对应的镜像可能被更新了。为了保证测试和部署的可重复性,所有的测试和部署都应该使用带标签的形式来引用镜像。

镜像标签最常用的格式是使用语义化版本号,目前绝大部分的公开镜像都使用版本号作为标签。在实际开发中,单纯使用版本号并不足以区分不同的构建版本,因为同一个版本在开发过程中可能多次构建。通常的做法是在版本号之后添加后缀,作为附加的区分信息。常用的后缀包括构建时间、构建编号和 Git 提交的标识符,如下表所示:

后缀说明
构建时间使用构建完成时的时间戳
构建编号持续集成服务为每个构建分配的标识符
Git 提交标识符触发持续集成的 Git 提交的标识符

在上表的这 3 种后缀形式中,并不推荐使用构建时间,因为时间本身并不能提供更多有用的信息。使用构建编号的好处是方便与已有的项目管理系统和 bug 追踪系统进行集成。测试团队一般工作在特定的构建编号之上。推荐的做法是使用 Git 提交的标识符作为后缀,从使用的镜像标签就可以快速定位到产生该镜像的代码。

我们可以使用 Maven 的 git-commit-id-maven-plugin 插件来获取 Git 中提交的标识符,并在构建过程中引用。

下面的代码给出了该插件的使用示例。该插件会提供一系列与 Git 相关的属性,比如 git.commit.id.abbrev 属性表示 Git 提交标识符的缩略形式。

<plugin> 
  <groupId>pl.project13.maven</groupId> 
  <artifactId>git-commit-id-plugin</artifactId> 
  <version>4.0.1</version> 
  <executions> 
    <execution> 
      <id>get-the-git-infos</id> 
      <goals> 
        <goal>revision</goal> 
      </goals> 
      <phase>initialize</phase> 
    </execution> 
  </executions> 
</plugin>

最终生成的镜像标签类似与于 1.0.0-3228a39。

使用 Jenkins

示例应用使用 Jenkins 作为持续集成的服务。Jenkins 使用 Helm 安装,运行在 Kubernetes 上。当需要运行构建任务时,Jenkins 会启动一个新的 Pod 来执行。

下面的代码是 Jenkins 中流水线的声明。构建的容器运行的是 Maven 的镜像,从 GitHub 获取源代码之后,通过 Maven 来执行构建过程,构建过程中会发布镜像到注册表中。

podTemplate(yaml: ''' 
apiVersion: v1 
kind: Pod 
spec: 
  securityContext: 
    fsGroup: 1000 
  containers: 
  - name: maven 
    image: maven:3.6.3-jdk-8 
    command: 
    - sleep 
    args: 
    - infinity 
    resources: 
      requests: 
        cpu: 1 
        memory: 1Gi 
    volumeMounts: 
      - name: dockersock 
        mountPath: "/var/run/docker.sock" 
  volumes: 
    - name: dockersock 
      hostPath: 
        path: /var/run/docker.sock 
''') { 
    node(POD_LABEL) { 
        git 'https://github.com/alexcheng1982/happyride' 
        container('maven') { 
            sh 'mvn -B -ntp -Dmaven.test.failure.ignore deploy' 
        } 
        junit '**/target/surefire-reports/TEST-*.xml' 
        archiveArtifacts '**/target/*.jar' 
    } 
}

这里需要着重介绍的是对 Docker 的使用。在构建过程中,单元测试和镜像发布都需要依赖 Docker 进程。在流水线的定义中,通过卷绑定的方式,把 Kubernetes 节点上的 /var/run/docker.sock 文件传到构建的 Pod 中,使得 Pod 中的容器可以访问节点上的 Docker 进程。通过这种方式,避免了在 Maven 容器中启动额外的 Docker 进程。

其他构建镜像的工具

目前所介绍的构建容器镜像的方式是使用 Docker,这也是目前比较主流的方式。Docker 在本地开发时很方便,但在持续集成中也有一些不足之处。

构建时需要有 Docker 守护进程存在,这就意味着需要在持续集成服务器上安装 Docker,并保持 Docker 在后台运行。如果在 Kubernetes 上运行,那么可以复用 Kubernetes 节点上的 Docker 进程。

另外一个问题来自 Docker 自身。随着多年的开发,Docker 自身已经过于臃肿,提供了非常多的功能。在持续集成中,我们只需要能够构建镜像,并推送到容器注册表即可,并不需要 Docker 提供的其他功能。

OCI 镜像规范的出现,使得容器镜像的格式不再锁定于特定的公司,从而促进了容器镜像构建相关工具的发展。目前已经有一些工具可以创建出 OCI 镜像,如下表所示。

名称支持者
BuildKitDocker Inc
buildahRed Hat
umociSUSE
KanikoGoogle
MakisuUber

在这些工具中,值得一提的是 Docker 的 BuildKit,其支持并发的构建,可以更高效地进行缓存,因此构建的速度更快。BuildKit 从 18.06 版本开始已经被集成到 Docker 中,可以通过下面的命令来启用 BuildKit。

DOCKER_BUILDKIT=1 docker build

有一些其他工具对 BuildKit 进行了封装,可以简化它的使用,包括 img、阿里巴巴的 pouch 和 Rancher 的 k3c 等。本课时不对这些工具进行具体的介绍,详细的使用请参考相关文档。

总结

在微服务架构中,持续集成可以保证每一个代码提交都可以有与之对应的容器镜像,容器镜像是不可变的,而且构建的过程是可重复的。通过本课时的学习,你可以了解如何从 Dockerfile 中创建出 Docker 容器镜像,以及如何使用 Spring Boot 插件和 Google 的 Jib 工具来创建 Java 应用的容器镜像,还可以了解如何使用 Jenkins 来进行持续集成。

最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价


第40讲:如何持续部署到阿里云

第 39 课时介绍了持续集成,本课时接着介绍如何实现持续部署,持续部署从容器镜像开始,把应用所有的微服务部署在云平台上。当有新的容器镜像被发布之后,持续部署负责更新应用。对于微服务架构的应用来说,持续部署需确保相互独立的各个微服务可以协同工作;对于多个微服务相互协作的场景,需要在持续部署的环境上进行测试。本课时介绍的持续部署的实现方式,不限定于特定的云平台,只需要有能够正常访问的 Kubernetes 集群即可。

每个微服务都需要独立部署,包括服务本身,以及服务依赖的支撑服务等。在这些支撑服务中,有些是微服务独有的,比如数据库;有些则是很多微服务共享的,比如 Apache Kafka 这样的消息代理。Kubernetes 上基本的部署方式是创建部署、有状态集和服务等资源,以 YAML 文件来描述,可直接通过 YAML 文件来创建资源的做法,只适合于非常简单的应用。当应用变得复杂时,需要 更 有效 的方式来管理部署相关的各种资源。目前在 Kubernetes 上最常用的部署方式是使用 Helm。

Helm 介绍

Helm 是 Kubernetes 上的软件包管理软件,类似于 Node.js 中的 npm 或 yarn,其已经成为 Kubernetes 上管理软件的事实上的标准。Helm 是 CNCF 中已经毕业的项目,由社区负责维护,目前有 2 和 3 两个主流版本,本课时以最新版本 3 为主。

Helm 中的每个软件包称为图表(Chart),它的一个优势是促进了应用软件包的共享。社区贡献了很多运行不同应用的图表,发布在公共的图表仓库中。绝大部分公开的应用,都可以在仓库中找到相应的 Helm 图表。通过 Helm Hub可以发布和查找 Helm 图表。

Helm 中有 3 个基本的概念,如下表所示。

概念说明
图表创建 Kubernetes 上应用的实例所需的信息集合
配置配置信息,与图表合并来创建可以发布的对象
发行图表的一个运行中的实例,与配置结合在一起

以 PostgreSQL 的 Helm 图表为例,该图表中包含了部署 PostgreSQL 所需的有状态集、配置表、服务和密钥等资源的声明;配置指的是根据应用部署的需要,为图表提供的配置项指定具体值,比如设置数据库的名称、访问数据库的用户名和密码等。把自定义配置值和图表结合起来,就得到了一个发行,可以在 Kubernetes 上运行。

我们可以在 Bitnami 的 Helm 图表仓库中找到 PostgreSQL 图表。在使用图表之前,首先需要把该 Helm 图表仓库添加到 Helm 中,如下面的代码所示。

$ helm repo add bitnami https://charts.bitnami.com/bitnami

每个 Helm 图表的根目录下都包含一个 values.yaml 文件,该文件中包含了图表提供的配置项及默认值。在以 helm install 命令来安装应用时,可以通过 --set 参数来指定配置项的值,或是通过 -f 参数来指定包含配置值的文件。提供的配置值会覆盖图表所提供的默认值。

下面代码中的 YAML 文件是安装 PostgreSQL 图表时使用的配置文件的内容。

postgresqlDatabase: testdb 
postgresqlUsername: myuser 
postgresqlPassword: mypassword

通过下面的命令安装 Helm 图表,第一个参数是发行的名称。

$ helm install test-postgresql -f values.yaml bitnami/postgresql

当需要对已有的应用发行进行修改时,可以使用 helm upgrade 命令,如下所示。在进行修改时,我们提供了新的配置文件。

$ helm upgrade test-postgresql -f values-v2.yaml bitnami/postgresql

Helm 负责记录每次对发行所做的修改,通过 helm history 命令可以查看一个发行的全部历史版本信息。如果对发行所做的修改产生了问题,可以通过 helm rollback 命令来回退到指定的版本。在下面的代码中,把发行 test-postgresql 回退到版本 1。

$ helm rollback test-postgresql 1

通过 helm uninstall 命令可以删除应用的发行,如下面的代码所示。

$ helm uninstall test-postgresql

除了上面提供的命令之外,常用的其他 Helm 命令如下表所示:

命令说明
list列出来所有的发行
plugin管理 Helm 插件
pull从仓库中下载图表文件到本地
search搜索图表
show显示图表的信息
status查看 Helm 发行的状态

创建 Helm 图表

虽然公开的 Helm 图表仓库中包含了常用服务的图表,但是安装 私有 应用的图表需要手动创建。以行程管理服务的 Helm 图表来进行说明,我们首先使用 helm create 命令来创建图表,如下面的代码所示。

$ helm create trip-service

该命令会在当前目录下创建一个名为 trip-service 的子目录,其中包含了 Helm 图表的基本代码。下面的代码给出了自动创建 的 图表所包含的目录和文件。

  .helmignore 
  Chart.yaml 
  values.yaml 
 
├─charts 
└─templates 
      deployment.yaml 
      hpa.yaml 
      ingress.yaml 
      NOTES.txt 
      service.yaml 
      serviceaccount.yaml 
      _helpers.tpl 
     
    └─tests 
            test-connection.yaml

下表给出了这些目录或文件的说明。

文件或目录说明
Chart.yaml图表的元数据,包括名称、描述、类型和版本号
values.yaml图表提供的配置项及其默认值
charts所依赖的其他图表
templates资源的模板

对一个 Helm 图表来说,最重要的是 templates 目录中包含的模板文件。下表给出了 templates 目录下的 子 目录或文件的说明。

文件或目录说明
_helpers.tpl包含可复用的变量的声明
YAML文件Kubernetes 资源声明模板
NOTES.txt图表安装之后显示的内容
tests图表测试用例

templates 目录下的每个模板文件都会被转换成 YAML 文件,并应用到 Kubernetes 中。一个模板文件中通常包含一个 Kubernetes 资源,在模板文件中可以引用 values.yaml 文件中的 配置项 。Helm 提供了很多内置的函数来生成模板中的内容。

由 helm create 命令创建的图表用来安装 Nginx,从 templates 目录中可以看到与 Kubernetes 中的部署、Ingress、服务、服务账户和 Pod 自动水平扩展相关的资源。

对行程管理服务来说,我们只需要对生成的图表中的 Kubernetes 部署的模板进行修改即可。 图表的完整代码请参考 GitHub 上源代码中的 K 8s 目录。

Helmfile 介绍

通过 Helm 的图表可以方便地安装单个应用。但是当需要同时安装多个互相关联的应用时,单独使用 Helm 很难进行管理。在安装 Helm 的图表时,配置项的值通过 YAML 文件来传递,一个常见的需求是在安装两个不同的图表时,使用同样的配置值。一个典型的场景是访问数据库的用户名和密码,同样的用户名和密码,在 PostgreSQL 的图表,以及使用该 PostgreSQL 的行程管理服务的图表中,都会被用到。我们希望的做法是只在一个地方维护这些配置项的值,不仅减少了代码重复,配置修改时也会变得更简单。

在目前的版本中,Helm 并没有提供一种比较有效的方式来在两个独立的图表之间共享配置。 这主要是因为 Helm 的 values.yaml 文件不支持模板的语法,必须是实际的配置值。在 Helm 的 GitHub 上,2017 年就有人提出了这个问题,但是 Helm 一直没有解决。 比较可行的做法是把 PostgreSQL 的图表作为行程管理服务的子图表,这样以全局变量的形式在父图表和子图表之间传递值。不过这种做法的限制比较多,有些图表之间并不存在直接的父子关系。Helmfile是解决这一问题的工具。

Helmfile 通过 helmfile.yaml 文件来管理多个 Helm 发行。下面的代码是行程管理服务使用的 helmfile.yaml 文件的内容,在这个文件中,repositories 用来声明获取 Helm 图表的仓库,这里定义了 Bitnami 的仓库。releases 用来声明 Helm 发行,这里定义了两个发行:第一个名为 postgresql-trip 的发行用来安装 PostgreSQL,指定了图表的名称和版本;第二个名为 trip-service 的图表用来安装行程管理服务 ,使用的是 charts 子目录中的自定义 Helm 图表 trip-service 。

repositories: 
  - name: bitnami 
    url: https://charts.bitnami.com/bitnami 
releases: 
  - name: postgresql-trip 
    namespace: {{ env "NAMESPACE" | default "happyride" }} 
    chart: bitnami/postgresql 
    version: 8.10.13 
    wait: false 
    values: 
      - ../postgresql-config.yaml 
      - config.yaml 
  - name: trip-service 
    namespace: {{ env "NAMESPACE" | default "happyride" }} 
    chart: charts/trip-service 
    values: 
      - config.yaml 
      - image: 
          tag: {{ requiredEnv "TRIP_SERVICE_VERSION" | quote }}
        resources: 
          requests: 
            memory: "512Mi" 
            cpu: "500m" 
          limits: 
            memory: "1Gi" 
            cpu: "1"

从该文件中可以看出 Helmfile 的一些优势:

  • Helmfile 文件本身可以使用与 Helm 相似的模板语法;

  • 通过 env 函数可以从环境变量中获取值;

  • 可以通过 values 来使用多个配置项的来源,postgresql-trip 发行中的配置项来自两个 YAML 文件,而 trip-service 发行中的配置项来自配置文件和内联的值,Helmfile 会自动对配置项进行合并。

Helmfile 简化了不同发行之间的配置项的共享。在上面的 helmfile.yaml 文件中,两个发行都用到了 config.yaml 文件,该文件中包含了 PostgreSQL 数据库相关的配置,被两个发行所共享。而 postgresql-config.yaml 文件则包含了与 PostgreSQL 相关的全局配置,该文件会被所有的 PostgreSQL 发行所共享。

在创建了 helmfile.yaml 文件之后,使用 helmfile apply 命令可以通过 Helm 来安装应用。

除了应用自身的服务之外,第三方支撑服务也可以使用 Helmfile 来安装。下面代码中的 helmfile.yaml 文件用来安装 Apache Kafka。

repositories: 
  - name: bitnami 
    url: https://charts.bitnami.com/bitnami 
releases: 
  - name: kafka 
    namespace: {{ env "NAMESPACE" | default "happyride" }} 
    chart: bitnami/kafka 
    version: 11.3.2 
    wait: true

当存在多个应用时,每个应用的 helmfile.yaml 文件可以组织在一起,由另外一个 helmfile.yaml 文件来管理。下面的代码给出了示例应用中不同服务的 helmfile.yaml 文件的组织结构,其中的 apps 子目录包含了每个应用的 helmfile.yaml 文件和 Helm 图表。

. 
├── apps 
   ├── address-service 
      ├── address-service-config.yaml 
      ├── charts 
      ├── config.yaml 
      └── helmfile.yaml 
   ├── axon 
      ├── charts 
      └── helmfile.yaml 
   ├── common 
      ├── charts 
      └── helmfile.yaml 
   ├── kafka 
      └── helmfile.yaml 
   ├── passenger-api-graphql 
      ├── charts 
      └── helmfile.yaml 
   ├── passenger-service 
      ├── charts 
      ├── config.yaml 
      └── helmfile.yaml 
   ├── postgresql-config.yaml 
   ├── redis 
      └── helmfile.yaml 
   └── trip-service 
       ├── charts 
       ├── config.yaml 
       └── helmfile.yaml 
└── helmfile.yaml

下面是根目录 中的 helmfile.yaml 文件的内容,使用通配符包含了 apps 目录下的全部 helmfile.yaml 文件。

helmfiles: 
- apps/*/helmfile.yaml

当在根目录下运行 helmfile apply 命令时,全部的应用都会更新。

持续部署

每个微服务都应该有自己的持续部署流程,对于每个服务来说,代码提交会触发持续集成流程。当持续集成完成之后,该服务的容器镜像会被发布到镜像注册表中,并且由一个唯一的标签来标识。在安装应用的 Helm 图表中,镜像的标签通常以 image.tag 配置项来传递。只需要更新该配置项的值,再使用 Helm 来更新应用的发行,就可以部署新的版本。

不同的环境可能 有 各自的持续部署策略。对于开发环境来说,每次代码提交都可以触发部署流程;对于测试环境来说,由于测试周期的问题,测试团队不会频繁更新部署;对于生产环境来说,部署会有更加严格的控制策略。

在进行部署时,所需要的输入只有镜像的标签,实际的部署操作由 Helmfile 来完成。下面的代码给出了部署地址管理服务的命令。 环境变量 ADDRESS_SERVICE_VERSION 的值会被传递给对应 Helm 图表的 image.tag 配置项。

$ ADDRESS_SERVICE_VERSION=1.1.0-6ec24a6 helmfile apply

为了部署可以成功,Helmfile 在运行时需要访问 Kubernetes 集群。如果 kubectl 可以成功访问 Kubernetes 集群,那么同一机器上的 Helmfile 也能正常访问。如果在 Kubernetes 集群内部的 Pod 容器中进行部署,那么需要注意权限的问题。Pod 运行时默认的服务账户可能没有修改 Kubernetes 资源的权限。

下面代码中的 YAML 文件创建了一个服务账户 deploy-user,并且赋予了该账户 cluster-admin 角色,允许访问 Kubernetes 上的任意资源。进行部署工作的 Pod 可以使用该服务账户。 如果需要进一步控制该部署服务账户的权限,可以使用 Kubernetes 提供的 RBAC 支持。

apiVersion: v1 
kind: ServiceAccount 
metadata: 
  name: deploy-user 
  namespace: happyride 
--- 
apiVersion: rbac.authorization.k8s.io/v1 
kind: ClusterRoleBinding 
metadata: 
  name: deploy-user 
roleRef: 
  apiGroup: "" 
  kind: ClusterRole 
  name: cluster-admin 
subjects: 
  - kind: ServiceAccount 
    name: deploy-user 
    namespace: happyride

下面介绍 Jenkins 上的持续部署的流水线。整个流水线由两个阶段组成:构建阶段负责构建容器镜像并发布到镜像注册表,部署阶段负责调用 helmfile 来更新部署。

下面的代码是 Jenkins 的流水线配置。在 Pod 模板中,声明了使用服务账户 deploy-user。Pod 有两个容器,对应于构建和部署两个阶段,分别运行 Maven 和 Helmfile。在构建阶段中,容器镜像的标签被保存在 addressServiceImageTag 变量中;在部署阶段中,该变量的值被传递给环境变量 ADDRESS_SERVICE_VERSION。

addressServiceImageTag = '' 

pipeline {
  agent {
    kubernetes {
      yaml “”"
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: deploy-user
  securityContext:
    fsGroup: 1000
  containers:
  - name: maven
    image: maven:3.6.3-jdk-8
    command:
    - sleep
    args:
    - infinity
    resources:
      requests:
        cpu: “0.5”
        memory: 512Mi
      limits:
        cpu: “1”
        memory: 1Gi
    volumeMounts:
      - name: dockersock
        mountPath: “/var/run/docker.sock”
  - name: helmfile
    image: quay.io/roboll/helmfile:helm3-v0.125.0
    command:
    - sleep
    args:
    - infinity
    resources:
      limits:
        cpu: “0.5”
        memory: 256Mi
  volumes:
    - name: dockersock
      hostPath:
        path: /var/run/docker.sock
“”“

    }
  }
  stages {
    stage(‘Build’) {
      environment {
        BUILD_DOCKER = true
        CONTAINER_REGISTRY=‘docker-registry:5000’
      }
      steps {
        git ‘https://github.com/alexcheng1982/happyride’
        container(‘maven’) {
          sh ‘mvn -B -ntp -Dmaven.test.failure.ignore install’
          junit ‘**/target/surefire-reports/TEST-*.xml’
          script {
            addressServiceImageTag = readFile(“happyride-address-service/target/image_tag.txt”)
          }
        }
      }
    }
    stage(‘Deploy’) {
      environment {
        ADDRESS_SERVICE_VERSION = ”${addressServiceImageTag}"
        CONTAINER_REGISTRY = “localhost:30000”
      }
      steps {
        git ‘https://github.com/alexcheng1982/happyride’
        container(‘helmfile’) {
          sh ‘cd k8s/happyride/apps/address-service && helmfile apply’
        }
      }
    }
  }
}

下图是 Jenkins 上运行流水线的结果图。

1.png

总结

通过持续部署,每次代码提交所对应的代码,都可以在 Kubernetes 上部署运行。通过本课时的学习,你可以了解到 Helm 的用法、如何创建 Helm 图表,以及如何使用 Helmfile 来管理多个应用;同时还可以了解如何在 Jenkins 上实现服务的持续部署。

最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价


第41讲:如何结合服务网格进行灰度发布

上一课时介绍了如何实现持续部署,本课时将继续介绍持续部署相关的话题。在实际的生产环境部署中,不太可能一次性更新全部的后台服务的实例,而是需要逐步更新,本课时将对这种部署模式进行介绍。

灰度发布

灰度发布是国内特有的与发布相关的名词,从名称来说,灰度表示从白色到黑色的渐进过程,代表的是新版本的部署从完全未部署(白色)到完全部署(黑色)的过程。从含义上来说,灰度发布与我们通常说的金丝雀发布(Canary)、蓝绿发布(Blue/Green)、红黑发布(Red/Black)的含义是相似的。这些发布模式的共同点是,每次发布新版本时,不是一次性的全部更新,而是先进行部分更新,再逐步扩大更新的范围,最后完成全部的更新。

部分更新

这里提到的部分更新,实际上有两个维度:一个是从用户的角度来考虑的,当一个新的功能被开发并发布之后,一开始只有部分用户能够使用这个新功能,而在新功能的使用过程中,用户的反馈可以作为改进功能的基础。当新功能完善之后,可以对全部用户都启用该功能,最终完成整个更新过程。

另外一个维度是从应用运行时的实例的角度。一个应用运行时可能有多个实例,对应于 Kubernetes 上的多个 Pod。在新版本发布时,首先更新其中的一部分实例,再逐步增加更新到新版本的实例的数量,直到最后完成全部的更新。

这两种方式的区别在于用户的选择上。在第一种方式中,新功能的试用用户是精心挑选的,一般通过对用户的历史行为进行分析,来选择合适的用户,一个用户能否使用新功能的状态是固定不变的。在第二种方式中,用户是否可以使用新版本是随机的,取决于负载均衡器把用户的请求发送到当前版本还是新版本的运行实例。当用户在不同的时候访问服务时,他所看到的内容可能是不同的。

灰度发布与 Kubernetes 上部署的滚动更新机制是不同的。滚动更新在进行过程中,也是先更新一部分的 Pod 实例,再扩展至全部的 Pod 实例。但是在滚动更新中,新旧版本共存只是一种暂时的中间状态。Kubernetes 通过 Pod 中容器所定义的**探测器(Readiness Probe)**来判断新版本是否可用,并不会进行复杂的测试。在灰度发布中,新旧版本会共存一段时间,有专门的针对应用行为的测试。

下面对金丝雀发布、蓝绿发布和红黑发布进行介绍。

金丝雀发布

在 20 世纪初期,煤矿工人在下井之前,会先把金丝雀放入矿井,用来检测一氧化碳等有毒气体,进而提早发现问题。金丝雀发布由此而得名,在进行完整的版本更新之前,首先在新的环境上部署新版本,再把很小一部分的流量转到新版本,这部分的流量充当了金丝雀的作用。接着在一段时间之内,通过各种指标来比较新旧两个版本,当确认新版本没有问题之后,再把新版本的更新范围扩大,直到完成全部的部署。

蓝绿发布

在蓝绿发布的策略中,需要两个完全相同的生产环境,分别称为蓝色环境绿色环境。在这两个生产环境中,只有一个负责接受实际的请求,另外一个是交互准备环境。比如,如果蓝色环境是目前实际的生产环境,那么当需要发布新版本时,首先在绿色环境上进行部署和测试;当绿色环境测试完成之后,通过切换负载均衡器的方式,把请求转到绿色环境,之前的蓝色环境则处于闲置状态。等下一次部署时,蓝色环境就成为交互准备环境,蓝绿两个环境交替使用。当新版本运行的过程中出现问题时,可以快速切换到另外一个环境,从而回退到上一个版本。

下图是蓝绿发布的示意图,负载均衡器会轮流指向蓝绿两个不同的环境。

Drawing 1.png

红黑发布

红黑发布与蓝绿发布存在一些相似之处。下面是红黑发布的基本流程:

  1. 目前在生产环境上运行的称为红色分组;

  2. 创建新版本的生产环境,与之前的版本同时运行,这种状态称为红/红状态;

  3. 把流量从当前版本切换到新版本,此时的状态称为黑/红状态;

  4. 如果新版本运行正常,则删除之前的版本,只保留一个版本,为下一次发布做准备。

下图给出了红黑发布的示意图,按照从上到下的顺序对应于上述的 4 个步骤。

Drawing 3.png

与蓝绿发布的区别在于,红黑发布在大部分情况下只有一个生产环境,也就是红色环境。只有在部署过程中,才会同时存在红黑两个环境,黑色环境的存在是暂时的。与蓝绿发布相比,红黑发布适合于发布频率较低的情况;如果发布的频率很高,那么蓝绿发布的模式更加适用,因为不需要重复的创建和销毁环境。

使用服务网格

对于上面的这些发布方式,都要求控制流量在不同目的地之间的分配,这刚好是服务网格可以起作用的地方。下面以蓝绿发布为例,来说明如何通过服务网格实现。

蓝绿部署

在进行部署时,我们需要准备蓝绿两个环境,这在 Kubernetes 上很容易实现,只需要创建两个不同的部署即可。为了支持这种部署方式,需要把 Helm 图表分成两个,一个用来创建部署,另外一个用来创建服务。

在之前创建的地址管理服务的 Helm 图表中,删除掉 templates 目录下的 service.yaml 文件,同时在 _helpers.tpl 文件中定义两个新的变量 selectorLabelsWithDeploymentType 和 nameWithDeploymentType,如下面的代码所示。这两个变量都引用了配置项 deploymentType,表示部署的类型,可选值是 blue 和 green,分别表示蓝色和绿色部署。

变量 selectorLabelsWithDeploymentType 用在部署的 spec.selector.matchLabels 属性中,用来选择部署中的 Pod 实例,不同类型部署的 Pod 实例是分开的;nameWithDeploymentType 变量作为部署的名称。

{{/* 
Selector labels with deployment type 
*/}} 
{{- define "address-service.selectorLabelsWithDeploymentType" -}} 
{{ include "address-service.selectorLabels" . }} 
app.vividcode.io/deployment-type: {{ .Values.deploymentType | quote }} 
{{- end }} 
{{/* 
Create the service name with deployment type 
*/}} 
{{- define "address-service.nameWithDeploymentType" -}} 
{{- printf "%s-%s" (include "address-service.name" .) .Values.deploymentType }} 
{{- end }}

另外一个名为 address-service-common 的 Helm 图表中包含了 Kubernetes 中服务的声明,该声明是蓝绿两个部署所共用的,服务的名称固定为 address-service。服务的选择器的声明如下所示,从中可以看到,选择器只根据应用的名称来进行选择。

{{/* 
Selector labels 
*/}} 
{{- define "address-service-common.selectorLabels" -}} 
app.kubernetes.io/name: {{ include "address-service-common.serviceName" . }} 
{{- end }}

下面是地址管理服务对应的 helmfile.yaml 文件的内容,该文件定义了 3 个 Helm 发行,分别对应于 PostgreSQL 数据库、address-service-common 图表和地址管理服务本身。环境变量 DEPLOYMENT_TYPE 表示部署的类型,该变量的值作为 Helm 发行的名称的一部分,同时也传递给 Helm 图表中的配置项 deploymentType。

repositories: 
- name: bitnami 
  url: https://charts.bitnami.com/bitnami 
releases: 
  - name: postgresql-address 
    namespace: {{ env "NAMESPACE" | default "happyride" }}
    chart: bitnami/postgresql 
    version: 8.10.13
    wait: false 
    values: 
      - ../postgresql-config.yaml 
      - config.yaml 
  - name: address-service-common 
    namespace: {{ env "NAMESPACE" | default "happyride" }}
    chart: charts/address-service-common
  - name: address-service-{{ requiredEnv "DEPLOYMENT_TYPE" }}
    namespace: {{ env "NAMESPACE" | default "happyride" }}
    chart: charts/address-service 
    values: 
      - config.yaml 
      - address-service-config.yaml 
      - deploymentType: {{ requiredEnv "DEPLOYMENT_TYPE" | quote }}
        appVersion: {{ requiredEnv "ADDRESS_SERVICE_VERSION" | quote }}
        image: 
          repository: {{ printf "%shappyride/happyride-address-service" (env "CONTAINER_REGISTRY" | default "" ) | quote }}

下面的代码是 Kubernetes 部署的 YAML 文件的部分内容,对应于蓝色部署,从中可以看到不同标签的用法。

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: address-service-blue 
  labels: 
    helm.sh/chart: address-service-0.0.1 
    app.kubernetes.io/name: address-service 
    app.kubernetes.io/instance: address-service-blue 
    app.vividcode.io/deployment-type: "blue" 
    app.kubernetes.io/version: "1.0.0-fe220c2" 
    app.kubernetes.io/managed-by: Helm 
spec: 
  replicas: 1 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: address-service 
      app.kubernetes.io/instance: address-service-blue 
      app.vividcode.io/deployment-type: "blue" 
  template: 
    metadata: 
      labels: 
        app.kubernetes.io/name: address-service 
        app.kubernetes.io/instance: address-service-blue 
        app.vividcode.io/deployment-type: "blue"

在 helmfile 进行部署时,通过环境变量 DEPLOYMENT_TYPE 的不同值来触发蓝色或绿色部署,每个部署有各自的部署类型和版本号。

下面的代码是触发蓝色部署的命令的示例。

$ ADDRESS_SERVICE_VERSION=1.0.0-fe220c2 DEPLOYMENT_TYPE=blue helmfile apply

在完成部署之后,我们需要通过服务网格来控制流量。

基于百分比的流量控制

在服务网格的帮助下,可以很容易地实现灰度发布所需要的流量控制功能。

以 Istio 为例来进行说明,下面代码中的目的地规则定义了地址管理服务的两个子集,分别对应于蓝色和绿色的部署,通过特定的标签来选择。

apiVersion: networking.istio.io/v1beta1 
kind: DestinationRule 
metadata: 
  name: address-service 
spec: 
  host: address-service.happyride.svc.cluster.local 
  subsets: 
    - name: blue 
      labels: 
        app.vividcode.io/deployment-type: "blue" 
    - name: green 
      labels: 
        app.vividcode.io/deployment-type: "green"

下面的代码是地址管理服务对应的 Istio 中的虚拟服务。该虚拟服务定义了在蓝绿两个部署之间的流量分配策略,其中 99% 的请求会被发到当前版本对应的蓝色部署,剩下 1% 的请求才会被发到新版本对应的绿色部署。在一开始的时候,只有极少的请求会被发送到新版本的实例,这些请求充当了金丝雀的作用。通过这些请求,可以对新版本进行验证。

随着测试的进行,我们会对两个部署之间的流量分配进行调整。等测试完成之后,蓝色部署的 weight 值将变为 0,而绿色部署的 weight 值会变为 100,从而完成新旧两个版本的完全切换。

apiVersion: networking.istio.io/v1beta1 
kind: VirtualService 
metadata: 
  name: address-service-deployment 
spec: 
  hosts: 
    - address-service.happyride.svc.cluster.local 
  http: 
    - name: "address-service-http" 
      route: 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: blue 
          weight: 99 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: green 
          weight: 1

在完成一次版本更新之后,蓝绿部署的角色会进行互换。每次在进行新版本的部署时,总是从当前 weight 值为 0 的部署开始。

基于用户的流量控制

除了根据百分比来分配蓝绿两个部署的流量之外,还可以根据自定义的标识符来进行区分,从而允许为特定的用户启用新版本。当 API 网关接收到请求之后,可以根据当前的用户标识符来判断是否应该启用新版本,如果启用新版本,则在请求中添加自定义的 HTTP 头。服务网格根据该 HTTP 头来选择路由。

在下面代码的虚拟服务中,我们定义了两个路由。第一个路由使用自定义 HTTP 头 x-latest-version 来匹配,把请求发送到绿色部署对应的服务子集;第二个路由则默认把请求发送到蓝色部署对应的服务子集。

apiVersion: networking.istio.io/v1beta1 
kind: VirtualService 
metadata: 
  name: address-service-deployment 
spec: 
  hosts: 
    - address-service.happyride.svc.cluster.local 
  http: 
    - name: "latest" 
      match: 
        - headers: 
            x-latest-version: 
              exact: "true" 
      route: 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: green 
    - name: "current" 
      route:
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: blue

源代码管理

在实现灰度发布中的一个重要问题是如何与源代码管理系统进行集成。在两个版本同时部署和运行时,首先要做出的选择是,当新版本已经部分部署之后,是否还需要对当前版本进行修改,这个选择会确定后续的策略。

有些公司使用的是基于主干的开发方式(Trunk Based Development),也就是只有一个作为主干的源代码分支,所有开发都在这个分支上进行。在进行部署时,只需要从主分支中选择一个 Git 提交作为要部署的版本即可。在部署完成之后,代码的修改在主分支中进行。在下一次部署时,选择另外一个部署环境。

在下图中,圆圈表示 Git 提交,虚线表示把 Git 提交对应的代码部署到环境上。蓝色和绿色环境的部署交替进行。

Drawing 5.png

当需要开发一个较大的新功能时,所花费的时间可能很长。在新功能的开发过程中,仍然需要对当前的版本进行 bug 修复。这种情况下,基于主干的开发方式的管理会变得复杂,可以考虑使用分支。

新旧版本有各自的 Git 分支,当前版本的代码使用主分支,当需要开发新版本时,从主分支创建新的分支来进行开发,两个分支都有各自的持续集成和部署流程。在新版本部署之后,仍然需要对旧版本进行 bug 修复,新版本也需要根据用户的反馈进行修改。当新版本更新完成之后,其分支被合并到主分支,准备下一个版本的开发。

在下图中,当需要开发新功能时,从主分支中创建一个新分支,并部署到蓝色环境。与此同时,主分支的开发仍然在进行中,并部署到绿色环境。不过在主分支中所做的改动只限于严重 bug 的修复,大部分的开发仍然在新分支中进行。在主分支中所做的修改,需要被定期合并到新分支中,这样就确保了新分支中包含了全部相关的改动。当新分支开发完成,并合并到主分支之后,新分支的部署环境会变成当前的生产环境。

Drawing 7.png

新功能分支的版本号可以与主分支保持一致,也可以根据语义化版本的规范来更新版本号,是否更新版本号取决于改动的大小。新版本的分支都有各自的持续集成流程。由于持续集成所创建的容器镜像的标签使用 Git 提交的标识符作为后缀,因此不更新版本号也不会产生冲突。

总结

通过使用灰度发布,我们可以更加安全地对应用进行更新,不但可以进行更多的测试,当出现问题时还可以方便地回退部署。通过本课时的学习,你可以了解灰度发布相关的基本概念,还可以了解如何通过服务网格来实现,最后了解与灰度发布相对应的源代码管理策略。

最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐