注意:这不是完整的指南文章,而是对已经在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.yaml?Go模板引擎如何工作?我们已经在这里更详细地讨论了这些细节。该呼叫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发生更改,则可以遵循四个事件方案。考虑他们:- : ConfigMap, subPath.
 : .
- : ConfigMap, pod.
 : pod .
- : ConfigMap Deployment -.
 : , ConfigMap’, Deployment, pod , — .
- : 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"
)
type Config struct {
	Welcome string `json:"welcome"`
	Name    string `json:"name"`
}
var (
	globalConfig *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
}
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'
会发生什么?您可以在此处查看如何在更成人的项目(也就是“真实”项目)中解决类似情况(跟踪ConfigMap的更改)。重要!还记得本文中的所有上述内容对于Kubernetes(kind: Secret)中的Secret'ov也是适用的:它们与ConfigMap如此相似并非没有道理……奖金!第三方解决方案
如果您对跟踪配置中的更改感兴趣,则已经有现成的实用程序可用于此:使用附带的现有应用程序的容器启动此类应用程序将很方便。但是,如果您知道Kubernetes / ConfigMap和配置的功能不是``实时''(通过edit)编辑,而是仅作为部署的一部分...那么这些实用程序的功能似乎是多余的,即 复制基本功能。结论
随着Kubernetes中ConfigMap的出现,配置进入了下一轮开发:模板引擎的使用为它们带来了与HTML页面呈现相当的灵活性。幸运的是,这种复杂性并没有取代现有的解决方案,而是成为其补充。因此,对于认为新功能多余的管理员(甚至是开发人员),仍然可以使用好的旧文件。对于已经使用ConfigMap或仅查看ConfigMap的用户,本文简要概述了它们的本质和使用的细微差别。如果您对这个主题有自己的提示和技巧-我会很高兴在评论中看到。聚苯乙烯
另请参阅我们的博客: