company_banner

ConfigMaps в Kubernetes: нюансы, о которых стоит знать

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



    Предыстория: от rsync к… Kubernetes


    Что было раньше? В эпоху «классического администрирования» в простейшем варианте файл конфига размещали прямо рядом с приложениями (или в репозитории, если угодно). Всё просто: делаем элементарную доставку (CD) для нашего кода вместе с конфигом. Даже реализацию на условном rsync можно назвать зачатками CD.

    Когда инфраструктура вырастала, для разных сред (dev/stage/production) требовались разные конфиги. Приложение обучали понимать, какой конфиг использовать, передавая их в качестве аргументов к запуску или переменными среды, заданными в окружении. Ещё больше CD усложняется с появлением столь полезных Chef/Puppet/Ansible. У серверов появляются роли, а окружения перестают быть описанными в разных местах — мы приходим к IaC (Infrastructure as code).

    Что за этим последовало? Если удалось увидеть в Kubernetes критичные для себя плюсы и даже смириться с необходимостью модифицировать приложения для работы в этой среде, происходила миграция. По пути ожидало множество нюансов и отличий в построении архитектуры, но когда с основной частью удавалось справиться, получалось долгожданное приложение, запущенное в K8s.

    Оказавшись здесь, мы по-прежнему можем использовать конфиги, подготовленные в репозитории рядом с приложением или передавая ENV в контейнер. Однако в дополнение к этим способам стали также доступны ConfigMaps. Этот примитив K8s позволяет использовать Go templates в конфигах, т.е. рендерить их подобно HTML-страницам и делать reload приложения при изменении конфига без рестарта. С ConfigMaps больше нет потребности держать 3+ конфигов для разных окружений и следить за актуальностью каждого.

    Общую инструкцию-введение по ConfigMaps можно найти, например, здесь. А в этой статье я остановлюсь на некоторых особенностях работы с ними.

    Простейшие ConfigMap’ы


    Как же стали выглядеть конфиги в 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-шаблонизатор? Подробнее об этих деталях мы уже рассказывали здесь.

    Вызов 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.)

    Такие действия приведут к пересозданию pod’а с новым описанием. Например: был образ image:my-registry.example.com:v1, а стал — image:my-registry.example.com:v2. И совершенно не важно, что именно мы изменили в описании нашего Deployment’а: главное — что это вызвало пересоздание ReplicaSet (и, как следствие, pod’а). В таком случае новая версия конфиг-файла в новой версии приложения автоматически примонтируется и проблемы не будет.

    Реакция на изменение ConfigMap


    В случае появления изменений в ConfigMap’ах могут последовать четыре сценария событий. Рассмотрим их:

    1. Действие: исправлен ConfigMap, который смонтирован по subPath.
      Результат: файл конфига в контейнере не обновился сам.
    2. Действие: исправлен ConfigMap, а после его деплоя в кластер мы вручную удалили pod.
      Результат: новый pod монтирует новую версию ресурса сам.
    3. Действие: исправлен ConfigMap и аннотацией в Deployment мы завязались на его хэш-сумму.
      Результат: несмотря на то, что модификации сделаны только в ConfigMap’е, изменился и Deployment, поэтому старый pod был удалён, а новый — запущен с новой версией ресурса без ручного вмешательства.
    4. Действие: исправлен ConfigMap, смонтированный как каталог.
      Результат: файл конфига в pod’е обновился без рестарта/пересоздания pod’а.

    Разберем подробнее.

    Сценарий 1


    Мы правили только ConfigMap? Приложение не перезапустится. В случае с монтированием по subPath не будет никаких изменений до ручного рестарта pod’а.

    Тут всё просто: Kubernetes монтирует в pod наш ConfigMap определённой версии ресурса. Поскольку он смонтирован с subPath, никакого дополнительного «влияния» на конфиг больше не оказывается.

    Сценарий 2


    Не можем обновить файл без пересоздания pod’а? Окей, у нас в Deployment’е 6 реплик, поэтому мы можем по очереди, вручную сделать всем delete pod. Тогда при создании новых pod’ов они будут «забирать» новую версию ConfigMap’а.

    Сценарий 3


    Надоело выполнять подобные операции вручную? Вариант решения этой проблемы описан в Helm tips and tricks:

    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.

    Сценарий 4


    Возможно, приложение уже умеет следить за изменениями в конфиге и автоматически осуществлять reload? Здесь кроется немаловажная особенность ConfigMap’ов…

    В Kubernetes, если конфиг смонтирован с 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

      Обновим конфиг (через деплой или 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.

    Отслеживание изменений


    И напоследок — простой пример, как можно следить за изменениями в конфиге.

    Воспользуемся таким 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, смонтировав в pod конфиг ConfigMap’ом вместо файла из образа. На 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 не перезапускаются, т.к. для них никаких изменений в Deployment’е не произошло — они все ещё приветствуют 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’а — решается в более взрослом (да и просто «настоящем») проекте, можно здесь.

    Важно! Полезно также напомнить, что всё вышеописанное в статье справедливо и для Secret’ов в Kubernetes (kind: Secret): ведь не зря они так похожи на ConfigMap…

    Бонус! Сторонние решения


    Если вам интересна тема отслеживания изменений в конфигах, для этого есть уже готовые утилиты:

    • jimmidyson/configmap-reload — отправляет HTTP-запрос, если файл изменился. Разработчик планирует также научить отправлять и SIGHUP, но отсутствие коммитов с октября 2019 года оставляют эти планы под вопросом;
    • stakater/Reloader — следит за ConfigMap/Secrets и выполняет rolling upgrade (как называет его автор) над ресурсами, связанными с ними.

    Подобные приложения будет удобно запускать sidecar-контейнером к существующим приложениям. Однако, если знать особенности работы Kubernetes/ConfigMap и конфиги редактировать не «на живую» (через edit), а только в рамках деплоя… то возможности таких утилит могут показаться лишними, т.е. дублирующими базовые функции.

    Заключение


    С появлением ConfigMap в Kubernetes конфиги перешли на очередной виток развития: использование шаблонизатора принесло им гибкость, сопоставимую с рендерингом HTML-страниц. Благо, такие усложнения не заменили существующие решения, а стали их дополнением. Поэтому для администраторов (а скорее — даже разработчиков), которые считают новые возможности излишними, по-прежнему доступны старые добрые файлы.

    Для тех же, кто уже пользуется ConfigMap’ами или только присматривается к ним, в статье дан краткий обзор их сути и нюансов использования. Если же у вас есть свои tips & tricks по теме — буду рад увидеть в комментариях.

    P.S.


    Читайте также в нашем блоге:

    Флант
    DevOps-as-a-Service, Kubernetes, обслуживание 24×7

    Комментарии 16

      0
      Окей, у нас в Deployment’е 6 реплик, поэтому мы можем по очереди, вручную сделать всем delete pod.

      Вместо ручного удаления подов, лучше просто добавить/изменить аннотацию деплоймента (не все хелм используют), что-то рандомное:


      kubectl patch deploy <deployemnt-name> -p \
        "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"date\":\"`date +'%s'`\"}}}}}"
        0
        Как вариант. Если не пользуетесь helm.
        Если пользуетесь то checksum из пункта ниже подойдёт для этого лучше и он более близок к правильному решению чем рестарт pod'ов всегда с timestamp'ом в аннотации
          0

          С checksum файла, поды рестартанут даже после удаления пустой строки, форматирования, etc. Мы у себя использует хэши обьектов (хелм не используем) когда хотим автоматом рестарт.
          А патч хорош когда просто нужно сделать рестарт с RollingUpdate.

        0

        Важный момент с ConfigMap состоит в том, что в них нельзя положить файлы неограниченного размера.


        Так как ConfigMap хранятся в базе etcd, то лимит на их размер равен 1 мегабайту.

          0
          Тут вот предлагают увеличить этот лимит с помощью нового ресурса.
          +1

          Я конечно извиняюсь, но зачем ручное удаление пода? Можно же напрямую сказать "перезапусти deployment":


          kubectl rollout restart deployment deployment-name
            0

            стоит заметить, что это было добавлено в 1.15

            0
            Этот примитив K8s позволяет использовать Go templates в конфигах, т.е. рендерить их подобно HTML-страницам

            Кажется, рендеринг с помощью Go templates позволяет всё же Helm, а не непосредственно сам Кубернетес.
              0

              Да, я тоже хотел про это сказать… Спасибо, что опередили

              0
              Я использую такой костыль для обновления конфигов, в подах. Просто присваиваю переменную допустим в pipeline, и все.

              kubectl set env deployment/nginx --env='LAST_UPDATE'=$(date +%d.%m.%y-%H:%M:%S) --namespace=default
                0

                Ниже мое личное мнение. Не претендую на абсолютную истину.


                С появлением ConfigMap в Kubernetes конфиги перешли на очередной виток развития: использование шаблонизатора принесло им гибкость, сопоставимую с рендерингом HTML-страниц. Благо, такие усложнения не заменили существующие решения, а стали их дополнением. Поэтому для администраторов (а скорее — даже разработчиков), которые считают новые возможности излишними, по-прежнему доступны старые добрые файлы.

                Для собственных приложений конфигмапы не нужны. И даже более того вредны. Аргументы


                1. Чтобы перезапустить приложение при изменении конфигмапы, мы выяснили из статьи, необходимы костыли — вроде патча деплоймента хешом конфига или использование сторонних сайдкаров.
                2. конфигмапы ограничены по размеру
                3. как развитие темы конфигмапов — секреты — ничерта они в кластере не хранятся безопасным образом. Это фикция. Если мы хотим безопасно — нужно делать как Авито — выносить секреты во внешнее хранилище типа vault и оттуда его втаскивать.
                4. если мы используем хельм — мы с тем же успехом можем портянку конфигов инжектировать через env в самом описании деплоймента. Какая нам разница — вкидывать 1, 10 или 100 переменных, если это делает шаблонизатор?

                Зачем могут понадобиться конфигмапы — точно, если мы хотим засунуть в кубернетес кластер легаси. Ну, там nginx, постгрес или что-то подобное — что ест конфиги в виде файлов. Конфигмапу можно подключать как файл и это РЕАЛЬНО удобно. Но BEWARE! Subpath может дать очень неожиданные эффекты. С другой стороны, есть же сборки от bitnami, которые уже приведены к концепции 12 факторов и принимают конфиги не в виде файлов, а виде env'ов и тогда см. предыдущие соображения.


                Вот бест пректис и хотелось увидеть в настойщей статье, а не какие-то оторванные от жизни выкладки

                  0

                  Накину еще мыслю, пока не заснул. В случае prometheus и nginx — программ, которые умеют не применять конфиг, если он некорректный — можно на еще одну граблю наступить. Меняем мы configmap, reloader срабатывает и дает команду на применение нового конфига, но мы где-то облажались и конфиг неверный. Сервис его не применяет — как мы про это узнаем? Т.е. нам нужен еще один шаг — проверка того, что конфиг не применился.
                  Если же сделать как предложил я — зашить конфиг БЕЗ конфигмапы в само описание деплоймента — кубернетес увидит, что деплоймент поменялся, триггернет апдейт и поды начнут падать в случае кривого конфига, произойдет откат до предыдущей версии (* тут нюансы, но пойдет) — и мы увидим проблему.

                    +1

                    По мне так наоборот, писать конфиги напрямую в деплойменте это по меньшей мере неудобно.


                    Если у нас конфиги — это километровые yaml или, чем черт не шутит, легаси xml с вкраплением go темплейтинга — то манифест деплоймента превращается в нечитаемую кашу. А когда конфиги в конфигмапах в отдельных файлах в гите — это нагляднее, понятнее кто куда делал изменения, удобнее писать пайплайны (на определенные тесты при изменении конфигов и другие тесты при изменении деплойментов или других компонентов).


                    Плюс разграничения в правах. Допустим мы хотим девелоперам на этом окружении дать права на изменения конфигмапов, но не на изменения самих деплойментов.

                      0
                      Плюс разграничения в правах. Допустим мы хотим девелоперам на этом окружении дать права на изменения конфигмапов, но не на изменения самих деплойментов.

                      Это выглядит как минимум странно. Как они отлаживаться будут? Ну, и раз есть доступ к конфигмапам — значит, они смогут сломать этот деплоймент.


                      Если у нас конфиги — это километровые yaml или, чем черт не шутит, легаси xml с вкраплением go темплейтинга

                      Повторю свой тезис. Если это Легаси с конфиг файлами, неважно — хмл, свой формат, ямл или что-то другое — да, конфигмапы рулят, т.к. их можно смонтировать как файл. Не нужно переписывать докерфайл и энтрипойнт. Если же у нас софт свой со 100500 ручек — какая разница куда инжектировать параметры? Вы говорите всерьез, будто люди в деплойменте в кубере что-то глазами смотрят. Постоянно.


                      А когда конфиги в конфигмапах в отдельных файлах в гите — это нагляднее, понятнее кто куда делал изменения, удобнее писать пайплайны (на определенные тесты при изменении конфигов и другие тесты при изменении деплойментов или других компонентов).

                      Не аргумент, если используется хельм. Там все параметры в values.yaml сплошной портянкой.


                      Можете попробовать переубедить :-)

                        +1
                        Это выглядит как минимум странно. Как они отлаживаться будут? Ну, и раз есть доступ к конфигмапам — значит, они смогут сломать этот деплоймент.

                        Например, у меня есть неймспейс с системными утилитами, ну там кронджобы которые делают чистку артифкатори, которые суспендят окружения по вечерам и в конце недели и так далее. Я тоже не любитель конфиг файлов, считаю что параметризировать свои приложениия удобнее энв переменными. Но зачем мне давать доступ разработчикам к правке своих огромных манифестов, где сотни строк кода? Я им говорю, если вам нужно свою группу енвайронментов добавить в suspend-list — вот в этом секрете/конфигмапе одна строчка, добавьте туда через пробел регэксп на свои неймспейсы и все. Это гораздо проще как для разработчика так и для меня, повышается и читаемость кода и уменьшается шанс на ошибку.
                        А какие нибудь часто применяемые действия, например если разработчик хочет чтобы его энв сегодня саспендился в 23-00, а не в обычные 19-00, так вообще лучше на уровень аннотаций к неймспейсу выносить, чем править конфиг самого приложения.

                        Если же у нас софт свой со 100500 ручек — какая разница куда инжектировать параметры? Вы говорите всерьез, будто люди в деплойменте в кубере что-то глазами смотрят. Постоянно.

                        Люди не смотрят, а автоматизация смотрит. Допустим если у нас в коммите поменялся манифест деплоймента, то гитлаб-сиай установит новое окружение в отдельный неймспейс и прогонит инфраструктурные тесты. А если только файлик с конфигмапом, то деплоймент останется тот же и запустятся только тесты по хелсчекам самого приложения.

                        Не аргумент, если используется хельм. Там все параметры в values.yaml сплошной портянкой.

                        В Helm тоже можно использовать обвзяки, чтобы был не один сплошной values.yaml, а много values файлов где параметры сгруппированы по своим задачам/по смыслу.
                          0
                          Люди не смотрят, а автоматизация смотрит

                          автоматизация может посмотреть изменился ли сам деплоймент по факту или просто конфиг, даже если он переменными. Плюс конфиг в теории нужно валидировать — выше я уже писал, что там могут какую-либо белиберду написать и все сломается. Вопрос только лишь в сложности этой самой автоматизации.


                          А какие нибудь часто применяемые действия, например если разработчик хочет чтобы его энв сегодня саспендился в 23-00, а не в обычные 19-00, так вообще лучше на уровень аннотаций к неймспейсу выносить, чем править конфиг самого приложения.

                          У нас нет таких проблем. Развернули окружение. Оно посуществовало какое время. И убили его. Или автоматом подчистилось — в зависимости от того какое это окружение

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое