- PVSM.RU - https://www.pvsm.ru -
Примечание: это не полноценная статья-руководство, а скорее напоминание/подсказка для тех, кто уже пользуется ConfigMap в Kubernetes или только готовит своё приложение для работы в нём.

Что было раньше? В эпоху «классического администрирования» в простейшем варианте файл конфига размещали прямо рядом с приложениями (или в репозитории, если угодно). Всё просто: делаем элементарную доставку (CD) для нашего кода вместе с конфигом. Даже реализацию на условном rsync можно назвать зачатками CD.
Когда инфраструктура вырастала, для разных сред (dev/stage/production) требовались разные конфиги. Приложение обучали понимать, какой конфиг использовать, передавая их в качестве аргументов к запуску или переменными среды, заданными в окружении. Ещё больше CD усложняется с появлением столь полезных Chef/Puppet/Ansible. У серверов появляются роли, а окружения перестают быть описанными в разных местах — мы приходим к IaC (Infrastructure as code).
Что за этим последовало? Если удалось увидеть в Kubernetes критичные для себя плюсы и даже смириться с необходимостью модифицировать приложения для работы в этой среде, происходила миграция. По пути ожидало множество нюансов и отличий в построении архитектуры, но когда с основной частью удавалось справиться, получалось долгожданное приложение, запущенное в K8s.
Оказавшись здесь, мы по-прежнему можем использовать конфиги, подготовленные в репозитории рядом с приложением или передавая ENV в контейнер. Однако в дополнение к этим способам стали также доступны ConfigMaps [1]. Этот примитив K8s позволяет использовать Go templates в конфигах, т.е. рендерить их подобно HTML-страницам и делать reload приложения при изменении конфига без рестарта. С ConfigMaps больше нет потребности держать 3+ конфигов для разных окружений и следить за актуальностью каждого.
Общую инструкцию-введение по ConfigMaps можно найти, например, здесь [2]. А в этой статье я остановлюсь на некоторых особенностях работы с ними.
Как же стали выглядеть конфиги в Kubernetes? Что они получили от Go-шаблонов? Например, вот заурядный ConfigMap для приложения, разворачиваемого из Helm-чарта:
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-шаблонизатор? Подробнее об этих деталях мы уже рассказывали здесь [3].
Вызов pluck помогает выбрать из map’а нужную строку:
$ 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
Если при деплое изменить описание ресурса Deployment, то Kubernetes создаст новый ReplicaSet, уменьшая старый до 0 и увеличивая новый до указанного количества реплик. (Это справедливо для случая использования стратегии деплоя RollingUpdate [4].)
Такие действия приведут к пересозданию pod’а с новым описанием. Например: был образ image:my-registry.example.com:v1, а стал — image:my-registry.example.com:v2. И совершенно не важно, что именно мы изменили в описании нашего Deployment’а: главное — что это вызвало пересоздание ReplicaSet (и, как следствие, pod’а). В таком случае новая версия конфиг-файла в новой версии приложения автоматически примонтируется и проблемы не будет.
В случае появления изменений в ConfigMap’ах могут последовать четыре сценария событий. Рассмотрим их:
Разберем подробнее.
Мы правили только ConfigMap? Приложение не перезапустится. В случае с монтированием по subPath не будет никаких изменений до ручного рестарта pod’а.
Тут всё просто: Kubernetes монтирует в pod наш ConfigMap определённой версии ресурса. Поскольку он смонтирован с subPath, никакого дополнительного «влияния» на конфиг больше не оказывается.
Не можем обновить файл без пересоздания pod’а? Окей, у нас в Deployment’е 6 реплик, поэтому мы можем по очереди, вручную сделать всем delete pod. Тогда при создании новых pod’ов они будут «забирать» новую версию ConfigMap’а.
Надоело выполнять подобные операции вручную? Вариант решения этой проблемы описан в Helm tips and tricks [5]:
kind: Deployment
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
[...]
Таким образом, в шаблон pod’а (spec.template) просто прописывается хэш отрендеренного аннотацией конфига.
Аннотации — это произвольные поля key-value, в которых можно хранить свои значения. Если прописать их в шаблоне spec.template будущего pod’а, эти поля попадут в ReplicaSet и сам pod. Kubernetes заметит, что шаблон pod’а изменился (т.к. изменился sha256 конфига) и запустит RollingUpdate, в котором не меняется ничего кроме этой аннотации.
В результате, мы сохраняем ту же версию приложения и описания Deployment’а и по сути просто триггерим пересоздание pod’а автоматом — аналогичному тому, как делали бы вручную через kubectl delete, но уже «правильно»: автоматизированно и с RollingUpdate.
Возможно, приложение уже умеет следить за изменениями в конфиге и автоматически осуществлять reload? Здесь кроется немаловажная особенность ConfigMap’ов…
В Kubernetes, если конфиг смонтирован с subPath, он не обновится до рестарта pod’а (см. первые три сценария, рассмотренные выше). Но если смонтировать ConfigMap как каталог, без subPath, то внутри контейнера будет каталог с обновляющимся конфигом без рестарта pod’а.
Есть и другие особенности, про которые полезно помнить:
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
Обновим конфиг (через деплой или 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
Обратите внимание на изменившийся timestamp в каталоге, созданном Kubernetes.
И напоследок — простой пример, как можно следить за изменениями в конфиге.
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, смонтировав в pod конфиг ConfigMap’ом вместо файла из образа. На GitHub подготовлен пример Helm-чарта [6]:
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'
Что произойдет?
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’а — решается в более взрослом (да и просто «настоящем») проекте, можно здесь [8].
Важно! Полезно также напомнить, что всё вышеописанное в статье справедливо и для Secret’ов в Kubernetes (kind: Secret): ведь не зря они так похожи на ConfigMap…
Если вам интересна тема отслеживания изменений в конфигах, для этого есть уже готовые утилиты:
Подобные приложения будет удобно запускать sidecar-контейнером к существующим приложениям. Однако, если знать особенности работы Kubernetes/ConfigMap и конфиги редактировать не «на живую» (через edit), а только в рамках деплоя… то возможности таких утилит могут показаться лишними, т.е. дублирующими базовые функции.
С появлением ConfigMap в Kubernetes конфиги перешли на очередной виток развития: использование шаблонизатора принесло им гибкость, сопоставимую с рендерингом HTML-страниц. Благо, такие усложнения не заменили существующие решения, а стали их дополнением. Поэтому для администраторов (а скорее — даже разработчиков), которые считают новые возможности излишними, по-прежнему доступны старые добрые файлы.
Для тех же, кто уже пользуется ConfigMap’ами или только присматривается к ним, в статье дан краткий обзор их сути и нюансов использования. Если же у вас есть свои tips & tricks по теме — буду рад увидеть в комментариях.
Читайте также в нашем блоге:
Автор: Михаил Носов
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-administrirovanie/352553
Ссылки в тексте:
[1] ConfigMaps: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
[2] здесь: https://matthewpalmer.net/kubernetes-app-developer/articles/ultimate-configmap-guide-kubernetes.html
[3] здесь: https://habr.com/ru/company/flant/blog/423239/
[4] RollingUpdate: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
[5] Helm tips and tricks: https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
[6] пример Helm-чарта: https://github.com/flant/examples/tree/master/2020/04-configmaps
[7] fsnotify: https://github.com/fsnotify/fsnotify
[8] здесь: https://github.com/coreos/prometheus-operator/blob/release-0.14/contrib/prometheus-config-reloader/main.go#L223-L252
[9] jimmidyson/configmap-reload: https://github.com/jimmidyson/configmap-reload
[10] stakater/Reloader: https://github.com/stakater/Reloader
[11] Локальные файлы при переносе приложения в Kubernetes: https://habr.com/ru/company/flant/blog/471582/
[12] SSL-сертификаты от Let's Encrypt с cert-manager в Kubernetes: https://habr.com/ru/company/flant/blog/496936/
[13] Источник: https://habr.com/ru/post/498970/?utm_source=habrahabr&utm_medium=rss&utm_campaign=498970
Нажмите здесь для печати.