注意:这不是完整的指南文章,而是对已经在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的用户,本文简要概述了它们的本质和使用的细微差别。如果您对这个主题有自己的提示和技巧-我会很高兴在评论中看到。聚苯乙烯
另请参阅我们的博客: