云计算运维

Windows Server 2003 - Windows Server 2019 系统工具,Linux系统脚本,Mysql、Nginx、PHP、Redis、K8S、Seafile、Weblogic 、Jenkins、DNS、DHCP、FTP、IIS、Zookeeper、Rabbitmq、Oracle、Tomcat、Mavrn等服务搭建维护,请关注我.

[kustz] kustomize流水线和添加环境变量,ConfigMap,Secret的实现


大家好, 我是老麦, 一个运维小学生。

代码还是在 Github, 文章中有不清楚的可以上去看看

https://github.com/tangx/kustz/tree/chapter/04-kustomize

前面已经简单的封装了 Deployment, Service, Ingress, 完成了零部件的创建。

今天就通过 Kustomization 进行组装, 实现流水线。

Kustomize

开始之前, 先来安装 kustomize 库。

$ go get sigs.k8s.io/kustomize/v3

这里补充一下, 访问 Github https://github.com/kubernetes-sigs/kustomize/。

kustomize () 首页 README.md 并没有提到 go get 的包名。通常 k8s 的代码在 github 上都是镜像。这时候只需要进到 go.mod , 包名就一目了然。

// go.mod
module sigs.k8s.io/kustomize/v3

go 1.12

编码

先来看看 kustomization.yml 的定义, 非常的简单。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-demo
resources:
  - deployment.yml
  - service.yml
  - ingress.yml

今天的代码及其简单, 只需要 20 行搞定。在 import 的时候, 可能自动补全不会自己带上 v3。需要手工调整一下。

package kustz

import "sigs.k8s.io/kustomize/v3/pkg/types"

func (kz *Config) Kustomization() types.Kustomization {
 k := types.Kustomization{
  TypeMeta: types.TypeMeta{
   Kind:       types.KustomizationKind,
   APIVersion: types.KustomizationVersion,
  },
  Namespace: kz.Namespace,
  Resources: []string{
   "deployment.yml",
   "service.yml",
   "ingress.yml",
  },
 }

 return k
}

这里已经定了 kustomization 三个外部资源名字。

其它

kustomize 还是很贴心的, 在 types 把 version 和 kind 已经通过常量定义好了。

在 https://github.com/kubernetes-sigs/kustomize/blob/v3.3.1/pkg/types/kustomization.go

const (
 KustomizationVersion = "kustomize.config.k8s.io/v1beta1"
 KustomizationKind    = "Kustomization"
)

另外我们可以看到, 虽然 TypeMeta 定义相同, 但是直接从 apimachinery/pkg/apis/meta/v1.TypeMeta 复制过来的, 而不是通过引用。

// TypeMeta partially copies apimachinery/pkg/apis/meta/v1.TypeMeta
// No need for a direct dependence; the fields are stable.
type TypeMeta struct {
 Kind       string `json:"kind,omitempty" yaml:"kind,omitempty"`
 APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}

之前看到一句话, 简单的拷贝比引用可能更节约资源, 因为引用是初始化一整个包

测试

执行命令, 检查结果是不是和自己期待的一样。

$ go test -timeout 30s -run ^Test_KustzKustomize$ ./pkg/kustz/ -v

如果不是, 就回去检查代码吧。

3.2. [kustz] 使用 cobra 实现 kustz 命令, 让一切跑起来

有了前面几章的努力, 我们的命令行工具 kustz 终于要问世了。

代码还是在 Github, 文章中有不清楚的可以上去看看

https://github.com/tangx/kustz/tree/chapter/05-build-kustz-cli-with-cobra

kustz 命令

当前命令功能就很简单。

  1. default: 输出 kustz 默认配置
  2. render: 读取 kustz 配置并生成 kustomize 配置四件套。
$ kustz -h

Available Commands:
  default     在屏幕上打印 kustz 默认配置
  render      读取 kustz 配置, 生成 kustomize 所需文件

编码

本章的代码都很简单, 就是设计的文件比较多。

使用 cobra 创建命令

cobra 真的是一个非常好用的命令行工具。

go get -u github.com/spf13/cobra

在 /cmd/kustz/cmd/root.go 中创建 根命令 rootCmd。并定义 执行函数 Execute。

func Execute() error {
 return rootCmd.Execute()
}

在 /cmd/kustz/cmd/default.go 中创建 子命令 default, 无任何参数。在 /cmd/kustz/cmd/render.go 中创建 子命令 render, 有一个参数 config, 实现根据配置管理应用。

而在外部 /cmd/kustz/main.go 中, 只有一个入口函数 main 调用 rootCmd 的执行。保持文件清洁干爽。

花开两朵。在 /pkg/kustz/cmd.go 文件中, 提供了 函数 或 方法 供之前的命令调用

default

使用 go:embed 将配置文件 kustz.yml 嵌入到应用中。配置随着代码走, 测试分发两不误。

//go:embed kustz.yml
var defaultConfig string

func DefaultConfig() {
 fmt.Println(defaultConfig)
}

这里只是简单的配置文件打印标准输出。如果需要保存到文件, 用户可以自行使用 重定向符

render

通过 RenderAll 方法将之间的 Deployment, Ingress, Service, Kustomization 都保存成了对应文件。

func (kz *Config) RenderAll() error {
}

在 Kustomize 章节已经硬编码 Resources 的资源文件名称。因此这里可就定义了这几个文件名的常量。

const (
 FileDeployment    = "deployment.yml"
 FileIngress       = "ingress.yml"
 FileService       = "service.yml"
 FileKustomization = "kustomization.yml"
)

在 /pkg/kubeutils/yaml.go 中, 将 dep, svc, ing 等编码成 YAML 的时候, 用到了 sigs.k8s.io/yaml (k8syaml) 库, 而非 gopkg.in/yaml.v2 (pkgyaml)。跟踪一下 k8syaml 的代码就很容易知道, 前者在 pkgyaml 的基础上, 针对性的为 k8s 做了很多优化。

编译及测试

$ ./kustz render -c abc.yml

$ ./kustz default > abc.yml

$ go build ./cmd/kustz

3.3. [kustz] 为 Container 添加环境变量

再前面, 我们已经完成了 Deployment, Service, Ingress 和 Kustomization API 的封装。并通过 cobra 库创建了属于我们自己的 kustz 命令。

然而 kustz 的功能还简陋。今天我们就先来为容器添加环境变量。

代码还是放在 Github, https://github.com/tangx/kustz/tree/chapter/06-container-env

为容器设置环境变量

在官方文档中, 提高了两种为容器设置环境变量的方法

https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/

  1. env: 提供 k-v 模式 键值对。
    1. 值可以直接 value 提供。
    2. 也可以通过 valueFrom 从 secret 或 configmap 引用。
  2. envFrom: 从 secret 或 configmap 中读取键值对, 注入到容器中。。
    1. https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/

kubez.yml 配置

首先来看看 kubez.yml 的配置

# kubez.yml
service:
  envs:
    pairs:
      key1: value1
    files:
      - foo.yml
      - bar.yml

我设计了两种方式为容器提供环境变量。都是提供 k-v 模式。

# deployment.yml
    env:
    - name: DEMO_GREETING
      value: "Hello from the environment"
  1. pairs: k-v 模式。优先级更高, 可以覆盖 files 中出现的同名 k-v。
  2. files: 从文件中读取 k-v。
    1. 多个 kustz.yml 可以复用。
    2. 可以按类型分类, 更直观。例如工程变量和数据库变量。
    3. 选择 YAML 格式是为了更好的管理 值为多行的变量。比如证书。
    4. 同名变量,后者覆盖前者

挖个坑, 以后实现 2.2 中提到的数据库变量文件的加解密。让 GitOPS 更安全一点。

最后强调一下变量优先级顺序, 用链条表示: 后者覆盖前者

foo.yml <- bar.yml <- pairs

编码实现

在 /pkg/kustz/kustz.go 中, 增加配置字段。 这个很简单, 就不赘述了。

type ServiceEnvs struct {
 Pairs map[string]string `json:"pairs,omitempty"`
 Files []string          `json:"files,omitempty"`
}

在引用 ServiceEnvs 的时候没有始终指针。这样 Service 在初始化的时候, ServiceEnvs 即使在 kustz.yml 没有定义也会被初始化为  的零值。

type Service struct {
 Envs     ServiceEnvs `json:"envs,omitempty"`
}

代码还是很简单的, 在 /pkg/kustz/k_container.go 定义了方法 kubeContainerEnv。该方法中, 指定了变量的优先级。

func (kz *Config) kubeContainerEnv() []corev1.EnvVar {
 pairs := make(map[string]string, 0)

 for _, file := range kz.Service.Envs.Files {
  b, _ := os.ReadFile(file)
  _ = yaml.Unmarshal(b, &pairs)
 }

 for k, v := range kz.Service.Envs.Pairs {
  pairs[k] = v
 }

 return tokube.ContainerEnv(pairs)
}

新建了一个包 tokube, 这里面的函数通过 接受 参数返回 API 配置。

在 /pkg/tokube/container.go 定义了函数 ContainerEnvs 创建容器变量键值对。

func ContainerEnv(pairs map[string]string) []corev1.EnvVar {
 envs := []corev1.EnvVar{}
 for k, v := range pairs {
  envs = append(envs, corev1.EnvVar{
   Name:  k,
   Value: v,
  })
 }
 return envs
}

说一个容易错的点

在 Container API 中, 变量保存在 []corev1.EnvVar, 这是一个切片。切片的另一个 隐藏含义 就是可能出现 同名 KEY

第一次的代码如下,

func (kz *Config) kubeContainerEnv_Error() []corev1.EnvVar {
 envs := []corev1.EnvVar{}

// 注意这里所有变量全部假如了 envs 切片。
 envs = append(envs, tokube.ContainerEnv(kz.Service.Envs.Pairs)...)
 for _, file := range kz.Service.Envs.Files {
  b, _ := os.ReadFile(file)
  mm := make(map[string]string, 0)
  _ = yaml.Unmarshal(b, &mm)
  envs = append(envs, tokube.ContainerEnv(mm)...)
 }

 return envs
}

并没有测试这样的变量结构会出现什么情况, 因为这种情况就不应该出现。

测试

觉得之前的测试命令不方便, 更新了 Makefile, 添加了测试命令。执行命令测试吧。

$ make test.deployment

3.4. [kustz] ConfigMap 和 Secret 的生成器

大家好, 我是老麦, 一个运维小学生。 今天我们通过 kustomize 管理 ConfigMap 和 Secret

上一节我们通过 k-v 和 YAML文件 为容器添加环境变量。 同时也提到了可以通过 envFrom 这个关键字, 直接读取 ConfigMap 或 Secret 中的 k-v 作为容器的环境变量。

除了环境变量之外, ConfigMap 和 Secret 还能管理的东西还很多。 所以我个人觉得单应用管理部署的话, 对于配置的管理,还是比较重要的。

Kustomize 中的 ConfigMap Env File

在 kustzomize 中, ConfigMap 和 Secret 都是通过 生成器 Generator 管理的, 有很多配置。

https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/generatoroptions/

先切到 ConfigMapGenerator, 可以看到有三种模式提供数据, files , literalsenvs

如果按照我们之前说的, 为容器提供环境变量, 使用 envs 是最方便的。 从名字就可以看到, 就是为了环境变量而提供的。

https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/#configmap-from-env-file

但是这种模式提供数据有也有限制的

  1. 必须使用 key=value 这种结构
    1. 但这并不是 SHELL 变量赋值的形式
  2. 每一对 k-v 只能是单行。 key 作为变量名还好说, value 作为值就 不能支持多行 数据了。
    1. 另外 value 中的所有字符都是字面值。

举个例子

HTTPS_CERT="---RSA---\nasdfjal\n---END"

通常在 Shell 中 " 双引号 是可以保留 \n 换行符 转义的含义的。 但是在这里 " 和 \n 都是字面意思, 没有任何特殊。

ConfigMap / Secret 生成器

看看定义, ConfigMapArgs 和 SecretArgs

  1. 都是通过 GeneratorArgs 管理数据的。
  2. Secret 比 ConfigMap 多了一个 Type 字段。
type ConfigMapArgs struct {
 GeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
}

// SecretArgs contains the metadata of how to generate a secret.
type SecretArgs struct {
 GeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
 // This is the same field as the secret type field in v1/Secret:
 // It can be "Opaque" (default), or "kubernetes.io/tls".
 Type string `json:"type,omitempty" yaml:"type,omitempty"`
}

对于数据源我计划都从文件中读取。 这样三个模式就有了相同过的抽象结构体。 抽象结果为 Generator 结构体, 在 /pkg/kustz/kustz.go 中可以看到。

kustz.yml配置

  1. 在 kustz.yml 中新增加了两个字段 configmaps, secrets
  2. 每个字段都有三个模式 envs, files, literals
  3. 每个模式都有三个字段
    1. name: 最终生成的 ConfigMap 或 Secret 名字。
    2. files: 数据源。 [target_name=]source_name。 target_name 就是 ConfigMap 中的文件 key。 如省略, 默认与 source_name 相同。
    3. type: 类型。 Secret 专有。 取值范围参考 https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
# kustz.yml

# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/
configmaps:
  envs:
    - name: srv-webapp-demo-envs
      files:
        - src_name.txt

# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/secretgenerator/
secrets:
  literals:
    - name: srv-webapp-demo-literals
      files:
        - foo.yml
      # type: Opaque # default
  files:
    - name: srv-webapp-demo-files
      files:
      - tls.crt=catsecret/tls.crt
      - tls.key=secret/tls.key
      type: "kubernetes.io/tls"

编码

type Config struct {
 ConfigMaps Generator `json:"configmaps"`
 Secrets    Generator `json:"secrets"`
}

// Generator 定义数据源种类
type Generator struct {
 Literals []GeneratorArgs `json:"literals,omitempty"`
 Envs     []GeneratorArgs `json:"envs,omitempty"`
 Files    []GeneratorArgs `json:"files,omitempty"`
}

//  GeneratorArgs 定义数据源类型参数
type GeneratorArgs struct {
 Name  string   `json:"name,omitempty"`
 Files []string `json:"files,omitempty"`
 Type  string   `json:"type,omitempty"`
}

Generator 也就承担关于 ConfigMap 和 Secret 所有工作。

在 /pkg/kustz/k_kustomize.go 中, 为 Generator 创建了两个方法创建对应参数。

// toConfigMapArgs 返回 ConfigMap 参数
func (genor *Generator) toConfigMapArgs() []types.ConfigMapArgs {
 args := []types.ConfigMapArgs{}
 for _, data := range genor.datas() {
  for _, garg := range data.gargs {
   arg := tokust.ConfigMapArgs(garg.Name, garg.Files, data.mode)
   args = append(args, arg)
  }
 }
 return args
}
// toSecretArgs 返回 Secret 参数
func (genor *Generator) toSecretArgs() []types.SecretArgs {}

由于 ConfigMap 和 Secret 确实太过相似, 所以对于处理 GeneratorArgs 使用循环, 从而添加了一个 Mode 类型的概念。 这个 Mode 的取值范围就是 envs, literals, files

type GeneratorArgsData struct {
 mode  tokust.GeneratorMode
 gargs []GeneratorArgs
}

// datas 整合生成器数据
func (genor *Generator) datas() []GeneratorArgsData {
 return []GeneratorArgsData{
  {mode: tokust.GeneratorMode_Envs, gargs: genor.Envs},
  {mode: tokust.GeneratorMode_Files, gargs: genor.Files},
  {mode: tokust.GeneratorMode_Literals, gargs: genor.Literals},
 }
}

在 /pkg/tokust/generator.go 文件中, 定义了几个函数创建 kustomize 对象的方法。

func ConfigMapArgs(name string, files []string, mode GeneratorMode) types.ConfigMapArgs {
}
func SecretArgs(name string, files []string, typ string, mode GeneratorMode) types.SecretArgs {
 // 处理默认类型
 if typ == "" {
  typ = "Opaque"
 }
}

相应的, 也创建三种模式的对应的方法。

func generatorArgs_literals(name string, files []string) types.GeneratorArgs {
 data := make(map[string]string, 0)
 for _, file := range files {
  err := marshalYaml(file, data)
  if err != nil {
   panic(err)
  }
 }
 sources := mapToSlice(data)
 // ... 省略
}

func generatorArgs_files(name string, files []string) types.GeneratorArgs {
}

func generatorArgs_envs(name string, files []string) types.GeneratorArgs {
}

在 literals 中, 由于我们传入的是 文件, 但是在 kustomization.yml 是键值对。 所以多了一个读取数据的步骤, 并且定义了一个规则, 如果出现同名变量, 后面出现的覆盖先出现的

测试

执行命令, 查看结果。

$ make test.kustomize

这里不会直接生成 ConfigMap 和 Secret, 而是生成 Kustomization.yml 规则。

  • 分享:
评论
还没有评论
    发表评论 说点什么