Skip to content

第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 中,如下面的代码所示。

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

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

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

yaml
postgresqlDatabase: testdb 
postgresqlUsername: myuser 
postgresqlPassword: mypassword

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

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

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

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

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

java
$ helm rollback test-postgresql 1

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

java
$ helm uninstall test-postgresql

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

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

创建 Helm 图表

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

java
$ helm create trip-service

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

yaml
│  .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 。

yaml
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。

yaml
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 图表。

yaml
. 
├── 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 文件。

yaml
helmfiles: 
- apps/*/helmfile.yaml

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

持续部署

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

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

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

java
$ 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 支持。

yaml
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。

groovy
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 上运行流水线的结果图。

总结

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

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