Kubernetes ConfigMaps:要知道的细节

注意:这不是完整的指南文章,而是对已经在Kubernetes中使用ConfigMap或只是准备其应用程序以供使用的那些人的提醒/提示。



背景:从rsync到... Kubernetes


之前发生什么事了?在“经典管理”时代,最简单的版本是将配置文件放置在应用程序的旁边(如果需要,也可以放置在存储库中)。很简单:我们为代码和配置一起制作基本交付(CD)。甚至有条件的rsync实现都可以称为CD的雏形。

随着基础架构的发展,不同的环境(开发/阶段/生产)需要不同的配置。对应用程序进行了培训,以了解使用哪个配置,并将它们作为启动参数或环境中设置的环境变量传递。随着这样有用的Chef / Puppet / Ansible的出现,更多的CD变得更加复杂。角色出现在服务器上,环境不再在不同的地方描述-我们来到IaC(即代码基础架构)。

接下来是什么?如果有可能看到Kubernetes本身的关键优势,甚至需要修改应用程序使其在这种环境下工作,那么迁移就发生了。顺便说一句,我期望在架构的构造上有细微的差别和差异,但是当我设法应付主要部分时,我得到了期待已久的在K8s中运行的应用程序。

到达这里后,我们仍然可以使用在应用程序旁边的存储库中准备的配置,或者将ENV传递给容器。但是,除了这些方法之外,还可以使用ConfigMaps这个K8s原语允许您在配置中使用Go模板,即 呈现它们就像HTML页面一样,并在更改配置时重新加载应用程序而无需重新启动。有了ConfigMap,不再需要为不同的环境保留3个以上的配置并跟踪每个环境的相关性。

例如,可以在此处找到ConfigMap的一般介绍在本文中,我将重点介绍与它们合作的一些功能。

简单的ConfigMap


Kubernetes中的配置是什么样的?他们从模板中得到了什么?例如,这是从Helm图表部署的应用程序的普通ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app
data:
  config.json: |
    {
      "welcome": {{ pluck .Values.global.env .Values.welcome | quote }},
      "name": {{ pluck .Values.global.env .Values.name | quote }}
    }

此处的值替换为.Values.welcome并将.Values.name从文件中获取values.yaml为什么确切地来自values.yamlGo模板引擎如何工作?我们已经在这里更详细地讨论了这些细节

该呼叫pluck有助于从地图中选择必要的路线:

$ cat .helm/values.yaml 
welcome:
  production: "Hello"
  test: "Hey"
name:
  production: "Bob"
  test: "Mike"

此外,您可以同时使用配置的特定行和整个片段。

例如,ConfigMap可能像这样:

data:
  config.json: |
    {{ pluck .Values.global.env .Values.data | first | toJson | indent 4 }}

...以及values.yaml-以下内容:

data:
  production:
    welcome: "Hello"
    name: "Bob"

这里涉及的global.env是环境的名称。在部署期间替换此值,可以使用不同的内容呈现ConfigMap。first这里需要,因为 pluck返回一个列表,其第一个元素包含所需的值。

当有很多配置时


一个ConfigMap可以包含多个配置文件:

data:
  config.json: |
    {
      "welcome": {{ pluck .Values.global.env .Values.welcome | first | quote }},
      "name": {{ pluck .Values.global.env .Values.name | first | quote }}
    }
  database.yml: |
    host: 127.0.0.1
    db: app
    user: app
    password: app

您甚至可以分别挂载每个配置:

        volumeMounts:
        - name: app-conf
          mountPath: /app/configfiles/config.json
          subPath: config.json
        - name: app-conf
          mountPath: /app/configfiles/database.yml
          subPath: database.yml

...或使用目录一次获取所有配置:

        volumeMounts:
        - name: app-conf
          mountPath: /app/configfiles

如果在部署期间更改部署资源的描述,Kubernetes将创建一个新的ReplicaSet,将旧的ReplicaSet减少到0,并将新的ReplicaSet增加到指定的副本数。(对于部署策略而言确实如此RollingUpdate。)

此类操作将导致使用新的描述重新创建Pod。例如:有一个图像image:my-registry.example.com:v1,但变为- image:my-registry.example.com:v2而且,我们对部署的描述中所做的更改完全没有关系:主要的是,这导致重新创建了copySet(以及结果是pod)。在这种情况下,新版本的应用程序中的配置文件的新版本会自动挂载,不会有问题。

ConfigMap更改响应


如果ConfigMap发生更改,则可以遵循四个事件方案。考虑他们:

  1. : ConfigMap, subPath.
    : .
  2. : ConfigMap, pod.
    : pod .
  3. : ConfigMap Deployment -.
    : , ConfigMap’, Deployment, pod , — .
  4. : ConfigMap, .
    : pod’ / pod’.

我们将更详细地分析。

场景1


我们纠正了ConfigMap吗?该应用程序将不会重新启动。在进行安装的情况下,subPath除非手动重启吊舱否则不会进行任何更改。

一切都很简单:Kubernetes将ConfigMap挂载到特定版本资源的pod中。由于已安装subPath,因此不再提供对配置的其他“影响”。

方案2


不重新创建Pod便无法更新文件吗?好的,我们在Deployment中有6个副本,因此我们可以轮流手动进行所有操作delete pod然后,在创建新容器时,它们将“拾取”新版本的ConfigMap。

情况3


厌倦了手动执行此类操作?Helm提示和技巧中描述了此问题的解决方案

kind: Deployment
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
[...]

因此,spec.template仅将呈现的注释配置哈希值写入pod(模板中

注释是任意键值字段,您可以在其中存储值。如果将它们注册到spec.template将来的Pod 模板中,则这些字段将属于ReplicaSet和Pod本身。Kubernetes将注意到pod模板已更改(因为sha256配置已更改)并且将启动RollingUpdate,其中除此注释外没有其他更改。

结果,我们保存了相同版本的Deployment的应用程序和描述,并且实际上只是触发了pod的自动重新创建-类似于我们手动完成操作kubectl delete,但已经“正确”地进行了操作:自动并使用RollingUpdate

方案4


也许应用程序已经知道如何监视配置中的更改并自动重新加载?这是ConfigMaps的一个重要功能...

在Kubernetes中,如果使用config进行安装subPath,则在重启pod之前,它不会被更新(请参见上面讨论的前三个方案)但是,如果将ConfigMap挂载为不带的目录,subPath则容器内将存在一个目录,其中包含更新的配置,而无需重新启动pod。

还有其他一些要记住的功能:

  • 容器内部的此类更新的配置文件会有所延迟。这是由于文件未完全挂载,而是Kubernetes对象挂载。
  • 里面的文件是一个符号链接。范例subPath

    $ kubectl -n production exec go-conf-example-6b4cb86569-22vqv -- ls -lha /app/configfiles 
    total 20K    
    drwxr-xr-x    1 root     root        4.0K Mar  3 19:34 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    -rw-r--r--    1 root     root          42 Mar  3 19:34 config.json
    -rw-r--r--    1 root     root          47 Mar  3 19:34 database.yml

    没有subPath目录挂载时会发生什么

    $ kubectl -n production exec go-conf-example-67c768c6fc-ccpwl -- ls -lha /app/configfiles 
    total 12K    
    drwxrwxrwx    3 root     root        4.0K Mar  3 19:40 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    drwxr-xr-x    2 root     root        4.0K Mar  3 19:40 ..2020_03_03_16_40_36.675612011
    lrwxrwxrwx    1 root     root          31 Mar  3 19:40 ..data -> ..2020_03_03_16_40_36.675612011
    lrwxrwxrwx    1 root     root          18 Mar  3 19:40 config.json -> ..data/config.json
    lrwxrwxrwx    1 root     root          19 Mar  3 19:40 database.yml -> ..data/database.yml

    更新配置(通过deploy或kubectl edit),等待2分钟(apiserver缓存时间),然后瞧:

    $ kubectl -n production exec go-conf-example-67c768c6fc-ccpwl -- ls -lha --color /app/configfiles 
    total 12K    
    drwxrwxrwx    3 root     root        4.0K Mar  3 19:44 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    drwxr-xr-x    2 root     root        4.0K Mar  3 19:44 ..2020_03_03_16_44_38.763148336
    lrwxrwxrwx    1 root     root          31 Mar  3 19:44 ..data -> ..2020_03_03_16_44_38.763148336
    lrwxrwxrwx    1 root     root          18 Mar  3 19:40 config.json -> ..data/config.json
    lrwxrwxrwx    1 root     root          19 Mar  3 19:40 database.yml -> ..data/database.yml

    请注意Kubernetes创建的目录中更改的时间戳。

变更追踪


最后,一个简单的示例,说明如何监视配置中的更改。

我们将使用这样的Go应用程序
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/fsnotify/fsnotify"
)

// Config fo our application
type Config struct {
	Welcome string `json:"welcome"`
	Name    string `json:"name"`
}

var (
	globalConfig *Config
)

// LoadConfig - load our config!
func LoadConfig(path string) (*Config, error) {
	configFile, err := os.Open(path)

	if err != nil {
		return nil, fmt.Errorf("Unable to read configuration file %s", path)
	}

	config := new(Config)

	decoder := json.NewDecoder(configFile)
	err = decoder.Decode(&config)
	if err != nil {
		return nil, fmt.Errorf("Unable to parse configuration file %s", path)
	}

	return config, nil
}

// ConfigWatcher - watches config.json for changes
func ConfigWatcher() {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	done := make(chan bool)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				log.Println("event:", event)
				if event.Op&fsnotify.Write == fsnotify.Write {
					log.Println("modified file:", event.Name)
				}
				globalConfig, _ = LoadConfig("./configfiles/config.json")
				log.Println("config:", globalConfig)
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("error:", err)
			}
		}
	}()

	err = watcher.Add("./configfiles/config.json")
	if err != nil {
		log.Fatal(err)
	}
	<-done
}

func main() {
	log.Println("Start")
	globalConfig, _ = LoadConfig("./configfiles/config.json")
	go ConfigWatcher()
	for {
		log.Println("config:", globalConfig)
		time.Sleep(30 * time.Second)
	}
}

...使用这样的配置添加它:

$ cat configfiles/config.json 
{
  "welcome": "Hello",
  "name": "Alice"
}

如果运行,日志将为:

2020/03/03 22:18:22 config: &{Hello Alice}
2020/03/03 22:18:52 config: &{Hello Alice}

现在,我们将这个应用程序安装在Kubernetes中,并将ConfigMap配置而不是映像中的文件安装在pod中。在GitHub上准备了Helm图表示例

helm install -n habr-configmap --namespace habr-configmap ./habr-configmap --set 'name.production=Alice' --set 'global.env=production'

并仅更改ConfigMap:

-  production: "Alice"
+  production: "Bob"

例如,更新集群中的Helm图表,如下所示:

helm upgrade habr-configmap ./habr-configmap --set 'name.production=Bob' --set 'global.env=production'

会发生什么?

  • 应用程序v1和v2无法重新启动,因为 对于他们来说,部署没有任何变化-他们仍然欢迎Alice。
  • v3应用程序重新启动,重新读取配置,并向Bob致意。
  • v4应用程序未重新启动。由于ConfigMap是作为目录挂载的,因此可以注意到配置中的更改,并且可以即时更改配置,而无需重新启动Pod。是的,应用程序在我们的简单示例中注意到了更改-请参阅来自fsnotify的事件消息

    2020/03/03 22:19:15 event: "configfiles/config.json": CHMOD
    2020/03/03 22:19:15 config: &{Hello Bob}
    2020/03/03 22:19:22 config: &{Hello Bob}

您可以在此处查看如何在更成人的项目(也就是“真实”项目)中解决类似情况(跟踪ConfigMap的更改)

重要!还记得本文中的所有上述内容对于Kubernetes(kind: Secret中的Secret'ov也是适用的:它们与ConfigMap如此相似并非没有道理……

奖金!第三方解决方案


如果您对跟踪配置中的更改感兴趣,则已经有现成的实用程序可用于此:

  • jimmidyson / configmap-reload-如果文件已更改,则发送HTTP请求。开发人员还计划教SIGHUP发送,但是从2019年10月开始缺乏提交使这些计划成为问题;
  • stakater / Reloader-监视ConfigMap / Secrets并对其关联的资源执行滚动升级(如其作者所说)。

使用附带的现有应用程序的容器启动此类应用程序将很方便。但是,如果您知道Kubernetes / ConfigMap和配置的功能不是``实时''(通过edit编辑,而是仅作为部署的一部分...那么这些实用程序的功能似乎是多余的,即 复制基本功能。

结论


随着Kubernetes中ConfigMap的出现,配置进入了下一轮开发:模板引擎的使用为它们带来了与HTML页面呈现相当的灵活性。幸运的是,这种复杂性并没有取代现有的解决方案,而是成为其补充因此,对于认为新功能多余的管理员(甚至是开发人员),仍然可以使用好的旧文件。

对于已经使用ConfigMap或仅查看ConfigMap的用户,本文简要概述了它们的本质和使用的细微差别。如果您对这个主题有自己的提示和技巧-我会很高兴在评论中看到。

聚苯乙烯


另请参阅我们的博客:


All Articles