[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 命令
当前命令功能就很简单。
default
: 输出 kustz 默认配置。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/
env
: 提供k-v 模式
键值对。- 值可以直接
value
提供。 - 也可以通过
valueFrom
从 secret 或 configmap 引用。
- 值可以直接
envFrom
: 从 secret 或 configmap 中读取键值对, 注入到容器中。。- 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"
pairs
: k-v 模式。优先级更高, 可以覆盖 files 中出现的同名 k-v。files
: 从文件中读取 k-v。- 多个
kustz.yml
可以复用。 - 可以按类型分类, 更直观。例如工程变量和数据库变量。
- 选择
YAML
格式是为了更好的管理 值为多行的变量。比如证书。 - 同名变量,后者覆盖前者
- 多个
挖个坑, 以后实现
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
, literals
, envs
。
如果按照我们之前说的, 为容器提供环境变量, 使用 envs
是最方便的。 从名字就可以看到, 就是为了环境变量而提供的。
但是这种模式提供数据有也有限制的
- 必须使用
key=value
这种结构- 但这并不是 SHELL 变量赋值的形式
- 每一对
k-v
只能是单行。 key 作为变量名还好说, value 作为值就 不能支持多行 数据了。- 另外 value 中的所有字符都是字面值。
举个例子
HTTPS_CERT="---RSA---\nasdfjal\n---END"
通常在 Shell 中 " 双引号
是可以保留 \n 换行符
转义的含义的。 但是在这里 " 和 \n
都是字面意思, 没有任何特殊。
ConfigMap / Secret 生成器
看看定义, ConfigMapArgs 和 SecretArgs
- 都是通过
GeneratorArgs
管理数据的。 - 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
配置
- 在
kustz.yml
中新增加了两个字段configmaps, secrets
。 - 每个字段都有三个模式
envs, files, literals
。 - 每个模式都有三个字段
- name: 最终生成的 ConfigMap 或 Secret 名字。
- files: 数据源。
[target_name=]source_name
。 target_name 就是 ConfigMap 中的文件 key。 如省略, 默认与 source_name 相同。 - 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
规则。