在上一篇文章中,为大家介绍了 Helm 的初步使用。然而这仍然不能满足我司的工作流,主要问题有:

  1. Helm 不提供 apply 命令;因此在 CI/CD 场景中必须考虑到判断是 install 还是 upgrade。
  2. 不方便控制安装的 chart 版本;例如指定版本范围、锁定某一版本等。
  3. Values 必须是纯文本;不支持模板渲染、不方便区分环境。

因此我们需要 Helm Releases as Code。我听说过的产品有 HelmsmanHelmfile 两款。目前我们团队已经使用后者一段时间,并且有团队成员贡献过部分代码。

至于为什么选择 Helmfile,背后的真正原因是在发现 Helmfile 的时候还没听说过 Helmsman。后来了解 Helmsman 并尝试后发现它并没有解决我们后两个问题,因此也就没有替换了。

接下来我针对以上几个痛点来说说我们是怎么做的。

一键 Apply

Helmfile 的文档非常简明、直接,示例配置文件 就在 README 里。我节选一小段来说几个必要的配置项:

repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

releases:
  - name: my-release     # Release name
    namespace: staging   # Release namespace
    chart: bitnami/redis # Chart name
    values:              # 等效于 helm 的 --values 选项
      - foo.yaml
    set:                 # 等效于 helm 的 --set 选项
      - name: cluster.enabled
        value: "false"

将以上内容保存为 helmfile.yaml 文件,随后执行 helmfile apply 即可。Helmfile 将会帮我们:

  1. 添加 repositories 中声明的 Helm chart repo。
  2. 根据 release 小节内的配置,安装或更新 chart。

因此,上篇文章中提到的:

helm repo add ...
helm install ...
helm upgrade ...

可直接被简化为:

helmfile apply

同时,如果你安装了 helm-diff 插件,Helmfile 还会在执行操作前输出清晰的 diff:

具体安装过程本文不再详述。

Chart 版本控制

大多数社区提供的 charts 都采用 Semver 2.0 作为版本号。因此大多数情况下我们都希望锁定主版本,防止误升级引入 breaking change。Helmfile 提供了 version 参数可用于指定版本范围,例如:

# ...

releases:
  - name: my-release
    namespace: staging
    chart: bitnami/redis
    version: ^10.5.13 # 防止升级到 v11.x.x
    # ...

同时,Helmfile 还提供了 lock 文件,功能与常见版本管理器中的 lock 类似。配合 CI 时,除非提交代码改动 lock 文件,否则在任意时间点执行 CI 安装的 chart 版本是一致的。

你可以通过 helmfile deps 命令生成 lock,以上 helmfile.yaml 生成的示例 helmfile.lock 文件长这样:

version: v0.102.0
dependencies:
- name: redis
  repository: https://charts.bitnami.com/bitnami
  version: 10.5.13
digest: sha256:20f2840c2642bf98f03d2b5cf890c73e1f2c100a0f0475777ae7788b2a0ae98d
generated: "2020-03-24T14:14:01.663801+08:00"

可看出包含具体的 chart 版本、Helmfile 版本、哈希值、生成时间等参数。

当需要更新 lock 文件时,同样执行 helmfile deps 即可。

动态 Values

在 CI 上部署时,有些 values 的值不是固定的,可能来自于环境变量,也可能由于环境差异而不同。

环境变量

在之前的文章中我们介绍过 review apps,其中有一项很重要的需求是,每次开新分支部署的 release 不能同名,否则资源会因为重名而安装失败。所以我们要读取 GitLab CI 的 $CI_ENVIRONMENT_SLUG 环境变量,并拼接到最终的 Helm release name。因此可以这样做:

# ...

releases:
  - name: api-{{ requiredEnv "CI_ENVIRONMENT_SLUG" }}
    chart: ./deploy/chart
    # ...

其实 helmfile.yaml 是个 Go template 格式的模板,因此你还可以把环境变量传递给 values,也可以使用 if 之类的语法。例如:

# ...

releases:
  - name: my-release
    namespace: staging
    chart: bitnami/redis
    set:
      - name: cluster.enabled
        value: {{ requiredEnv "REDIS_CLUSTER_ENABLED" }}

区分环境

Helmfile 提供了名为 environments 的配置,此处并不是指环境变量,而是一个专属于 Helmfile 的概念。来看看例子。首先创建两个文件:

# environments/staging/values.yaml
releaseName: staging-release

# environment/production/values.yaml
releaseName: production-release

配置 helmfile.yaml

# ...

environments:
  staging:
    values: # 针对 staging 环境的 values,可通过 {{ .Environment.Values.* }} 读取
      - environments/staging/values.yaml
  production:
    values: # 针对 production 环境的 values,读取方式相同
      - environment/production/values.yaml

releases:
  - name: {{ .Environment.Values.releaseName }} # 引用 environment values 的值(例如 staging-release 或 production-release)
    namespace: {{ .Environment.Name }} # 引用 envrionment 名称(例如 staging 或 production)
    chart: bitnami/redis
    values:
      - values/{{ .Environment.Name }}/values.yaml # 根据环境名称读取不同的 Helm values 文件
{{ if eq .Environment.Name "production" }} # Go template 的 if 语句
      - values/production-specified-values.yaml # 仅用于 production 环境的 Helm values
{{ end }}

在执行 helmfile 时使用 -e 选项即可指定安装的环境:

helmfile -e staging apply

注意 -e 必须在子命令 apply 之前,它是一个全局选项。

小结

不得不说,Helmfile 的确很灵活,但采用 Go template + YAML 语法编写配置的方式稍有些难以阅读和维护。