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

В предыдущих статьях конфигурация добавлялась прямо в контейнеры при сборке или использовалась как есть в переменных окружения контейнеров при развёртывании.

Теперь для безопасности и гибкости конфигурация будет сохраняться в ConfigMap и Secret. А в дополнение к параметрам Helm-чарта (Values) и секретам werf будут продемонстрированы подходы параметризации и переиспользования конфигурации, а также хранения конфиденциальных данных вместе с кодом в Git-репозитории проекта.

Приложение в этой статье не предназначено для использования в production без доработки. Готовое к работе в production приложение мы получим в конце руководства.

Подготовка окружения

Если вы ещё не подготовили своё рабочее окружение на предыдущих этапах, сделайте это в соответствии с инструкциями статьи «Подготовка окружения».

Если ваше рабочее окружение работало, но перестало, или же последующие инструкции из этой статьи не работают — попробуйте следующее:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале статьи “Подготовка окружения”:

minikube start

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

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

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале статьи “Подготовка окружения”:

minikube start --namespace werf-guide-app

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

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

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>' \
  --docker-password='<ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите статью «Подготовка окружения» с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Обновим существующий репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/080_configuration/* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/080_configuration/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show

Не работает? Попробуйте инструкции на вкладке «Начинаю проходить руководство с этой статьи» выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован.
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/website $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app.
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/050_s3 ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий.
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/nodejs/080_configuration/* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован.
test -e ~/werf-guide/guides || git clone https://github.com/werf/website ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app.
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/examples/nodejs/050_s3 ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий.
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой статье, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/nodejs/080_configuration/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить.
git show --stat
# Показать изменения.
git show

ConfigMap и Secret

В Kubernetes есть специальные типы ресурсов ConfigMap и Secret, которые предназначены для отделения конфигурации, зависящей от среды и окружения, от образов контейнеров.

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

ConfigMap предназначен для хранения неконфиденциальных данных, в отличие от Secret, который используется для хранения различных типов секретов.

Подробнее про эти типы ресурсов можно почитать в официальной документации Kubernetes (ConfigMaps, Secrets). Далее мы разберём частые случаи их использования на примере нашего приложения.

Хранение конфигурационных файлов приложения в ConfigMap

Сейчас конфигурационный файл nginx.conf копируется в образ прямо во время сборки. Из-за этого при каждом его изменении будут происходить пересборка образа и перезапуск Pod’ов. Также сейчас нет простой возможности шаблонизировать nginx.conf.

Эти проблемы решаются, если перенести .werf/nginx.conf в отдельный ConfigMap — для того, чтобы монтировать nginx.conf во время деплоя, а не добавлять файл при сборке:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx.conf: |
    user nginx;
    worker_processes 1;
    pid /run/nginx.pid;

    events {
      worker_connections 1024;
    }

    http {
      include /etc/nginx/mime.types;
      default_type application/octet-stream;

      upstream backend {
        server 127.0.0.1:3000 fail_timeout=0;
      }

      server {
        listen 80;
        server_name _;

        root /www;

        client_max_body_size 100M;
        keepalive_timeout 10s;

        # По пути /static отдадим ассеты напрямую из файловой системы NGINX-контейнера.
        location /static {
          # В силу особенностей механизма сборки ассетов с Webpack клиент может хранить кэш ассетов сколь
          # угодно долго, не беспокоясь об их инвалидации.
          expires 1y;
          add_header Cache-Control public;
          add_header Last-Modified "";
          add_header ETag "";

          # Когда есть возможность, отдаём заранее сжатые файлы (вместо сжатия на лету).
          gzip_static on;

          access_log off;

          try_files $uri =404;
        }

        # Ассеты медиафайлов (картинки и т.п.) также отдадим из файловой системы NGINX-контейнера, но
        # отключим для них сжатие gzip.
        location /static/media {
          expires 1y;
          add_header Cache-Control public;
          add_header Last-Modified "";
          add_header ETag "";

          access_log off;

          try_files $uri =404;
        }

        # Все запросы, кроме запросов на получение ассетов, отправляются на Node.js-бэкенд.
        location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;

          proxy_pass http://backend;
        }
      }
    }
apiVersion: v1 kind: ConfigMap metadata: name: nginx-conf data: nginx.conf: | user nginx; worker_processes 1; pid /run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; upstream backend { server 127.0.0.1:3000 fail_timeout=0; } server { listen 80; server_name _; root /www; client_max_body_size 100M; keepalive_timeout 10s; # По пути /static отдадим ассеты напрямую из файловой системы NGINX-контейнера. location /static { # В силу особенностей механизма сборки ассетов с Webpack клиент может хранить кэш ассетов сколь # угодно долго, не беспокоясь об их инвалидации. expires 1y; add_header Cache-Control public; add_header Last-Modified ""; add_header ETag ""; # Когда есть возможность, отдаём заранее сжатые файлы (вместо сжатия на лету). gzip_static on; access_log off; try_files $uri =404; } # Ассеты медиафайлов (картинки и т.п.) также отдадим из файловой системы NGINX-контейнера, но # отключим для них сжатие gzip. location /static/media { expires 1y; add_header Cache-Control public; add_header Last-Modified ""; add_header ETag ""; access_log off; try_files $uri =404; } # Все запросы, кроме запросов на получение ассетов, отправляются на Node.js-бэкенд. location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://backend; } } }

Теперь добавим этот ConfigMap в Deployment, примонтировав его как файл внутрь контейнера frontend:

...
- name: frontend
  image: {{ .Values.werf.image.frontend }}
  ports:
  - containerPort: 80
  volumeMounts:
  - mountPath: /etc/nginx/nginx.conf
    subPath: nginx.conf
    name: nginx-conf
volumes:
- name: nginx-conf
  configMap:
    name: nginx-conf
... - name: frontend image: {{ .Values.werf.image.frontend }} ports: - containerPort: 80 volumeMounts: - mountPath: /etc/nginx/nginx.conf subPath: nginx.conf name: nginx-conf volumes: - name: nginx-conf configMap: name: nginx-conf

Не забудем удалить более ненужный файл .werf/nginx.conf, а также команду копирования этого файла в образ во время сборки, после чего сборка образа frontend станет выглядеть так:

...
# NGINX-образ с собранными ранее ассетами.
FROM nginx:stable-alpine as frontend
WORKDIR /www

# Копируем собранные ассеты из предыдушего сборочного образа.
COPY --from=builder /app/dist /www/static
... # NGINX-образ с собранными ранее ассетами. FROM nginx:stable-alpine as frontend WORKDIR /www # Копируем собранные ассеты из предыдушего сборочного образа. COPY --from=builder /app/dist /www/static

Переразвёртывание Deployment при изменении ConfigMap и Secret

По умолчанию, изменения в ConfigMap и Secret, примонтированных к Deployment, StatefulSet или DaemonSet, не приведут к перезапуску Pod’ов с новой конфигурацией. Чтобы Pod’ы обновились, их нужно аннотировать хеш-суммами всех используемых Pod’ом ConfigMap и Secret. Тогда при изменении ConfigMap и Secret аннотация изменится и Pod пересоздастся. Так выглядит аннотация с хеш-суммой ConfigMap с nginx.conf:

...
template:
  metadata:
    labels:
      app: werf-guide-app
    annotations:
      checksum/configmap-nginx: '{{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}'
... template: metadata: labels: app: werf-guide-app annotations: checksum/configmap-nginx: '{{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}'

Для каждого подключенного ConfigMap и Secret потребуется отдельная аннотация.

Авторы самоучителя предпочитают вместо аннотаций с хеш-суммами использовать операторы вроде stakater/Reloader, т.к. они проще, гибче и удобнее в работе.

Values

Использование Helm-чарта для развёртывания приложения даёт ряд преимуществ — например, возможность шаблонизировать манифесты. Ключевой встроенный объект шаблонизации — это Values, который предоставляет доступ к значениям, передаваемым в чарт из различных источников.

При использовании werf все данные, передаваемые в chart, можно условно разделить на несколько категорий:

Переиспользование конфигурации с Values и ConfigMap

Для упрощения конфигурации манифестов часто используемые параметры можно выносить в .helm/values.yaml:

...
mysql:
  storageSize: "100Mi"
... mysql: storageSize: "100Mi"

… и далее использовать в манифесте следующим образом:

...
volumeClaimTemplates:
- metadata:
    name: mysql-data
  spec:
    accessModes: ["ReadWriteOnce"]
    resources:
      requests:
        storage: "{{ .Values.mysql.storageSize }}"
... volumeClaimTemplates: - metadata: name: mysql-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: "{{ .Values.mysql.storageSize }}"

Это также позволит иметь разные значения для разных окружений, о чем более подробно будет рассказано в следующих разделах самоучителя.

Но особенно полезным будет перенос в .helm/values.yaml повторяющейся конфигурации, например, переменных окружения для приложения, которые мы используем в нескольких местах сразу:

...
app:
  envs:
    NODE_ENV: "production"
... app: envs: NODE_ENV: "production"

Теперь переменные окружения из .Values.app.envs можно либо подставлять в манифест в env контейнера, как было сделано ранее, либо вынести эти переменные окружения в ConfigMap, который потом подключить в контейнер через envFrom.

Первый вариант проще, но вариант с ConfigMap удобнее при большом количестве общих переменных окружения, т.к. позволит избежать их дублирования между контроллерами. ConfigMap может выглядеть так:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-envs
data:
  {{- range $key, $val := .Values.app.envs }}
  "{{ $key }}": "{{ $val }}"
  {{- end }}
apiVersion: v1 kind: ConfigMap metadata: name: app-envs data: {{- range $key, $val := .Values.app.envs }} "{{ $key }}": "{{ $val }}" {{- end }}

Подключаем этот ConfigMap через envFrom в Deployment как набор переменных окружения:

...
containers:
- name: backend
  image: {{ .Values.werf.image.backend }}
  command: ["node", "./bin/www"]
  ports:
  - containerPort: 3000
  envFrom:
  - configMapRef:
      name: app-envs
... containers: - name: backend image: {{ .Values.werf.image.backend }} command: ["node", "./bin/www"] ports: - containerPort: 3000 envFrom: - configMapRef: name: app-envs

Не забываем про аннотации с хеш-суммами, которые приведут к пересозданию Pod’ов при изменении ConfigMap:

...
template:
  metadata:
    labels:
      app: werf-guide-app
    annotations:
      checksum/configmap-nginx: '{{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}'
      checksum/configmap-app-envs: '{{ include (print $.Template.BasePath "/configmap-app-envs.yaml") . | sha256sum }}'
... template: metadata: labels: app: werf-guide-app annotations: checksum/configmap-nginx: '{{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}' checksum/configmap-app-envs: '{{ include (print $.Template.BasePath "/configmap-app-envs.yaml") . | sha256sum }}'

Аналогично этот ConfigMap подключается и в остальные ресурсы, в которых требуются те же переменные окружения.

Использование конфиденциальных данных с Values и Secret

Чтобы начать работу с секретами, сначала требуется сгенерировать симметричный ключ шифрования, что можно сделать командой werf helm secret generate-secret-key. Однако, поскольку мы уже заранее подготовили и зашифровали секреты, то и ключ шифрования мы сгенерировали за вас. Он находится в репозитории в файле .werf_secret_key и начнёт использоваться автоматически.

При работе с реальными приложениями ключ НЕЛЬЗЯ хранить в репозитории. Вместо этого рекомендуется передавать его через переменную окружения WERF_SECRET_KEY, храня его в безопасном месте. Подробнее про работу с ключами шифрования можно прочитать в документации werf.

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

{
  "development": {
    "username": "root",
    "password": "null",
    "database": "werf-guide-app",
    "host": "mysql",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "werf-guide-app",
    "host": "mysql",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": "password",
    "database": "werf-guide-app",
    "host": "mysql",
    "dialect": "mysql"
  }
}
{ "development": { "username": "root", "password": "null", "database": "werf-guide-app", "host": "mysql", "dialect": "mysql" }, "test": { "username": "root", "password": null, "database": "werf-guide-app", "host": "mysql", "dialect": "mysql" }, "production": { "username": "root", "password": "password", "database": "werf-guide-app", "host": "mysql", "dialect": "mysql" } }

… мы будем хранить их зашифрованными в .helm/secret-values.yaml вместе с другими секретными параметрами:

app:
  secretEnvs:
    S3_USERNAME: 10008f28d41aa1f99901bd9581863d4668cd9ffde5e9262d5495cf7acab027903721
    S3_PASSWORD: 10007146b848eb0c24116ef18377394efceb096ee2718208dd7a9c209fe3a8f95c07
    DB_USERNAME: 1000bcd85fafffbaaec5bc11152307b7503e9764e737264ab7bc44240652394e374d
    DB_PASSWORD: 1000687e9b8472b98dc7395b09b7fdc628cdff5b6dc1e1a548ba6bffe76c30874f4b
    SECRET_KEY_BASE: 1000921032b2b78973963c1a2d0b6bc5ec98bfbde55cbd4c49c32ebaed73f8d6a736
mysql:
  secretEnvs:
    MYSQL_ROOT_PASSWORD: 1000f29b2bd6877c2f690d7fe82822a590bfd154db2d947102eb4341722cf564f635
minio:
  secretEnvs:
    MINIO_ROOT_USER: 1000ee40b96582f838ebbf4fb245ea15d912f0a1e76e3ba8461078a3078c06764315
    MINIO_ROOT_PASSWORD: 1000b7467033b6227b7a2052ac60e16e34382f26dee78f4015276b01b1b5c25a887a
app: secretEnvs: S3_USERNAME: 10008f28d41aa1f99901bd9581863d4668cd9ffde5e9262d5495cf7acab027903721 S3_PASSWORD: 10007146b848eb0c24116ef18377394efceb096ee2718208dd7a9c209fe3a8f95c07 DB_USERNAME: 1000bcd85fafffbaaec5bc11152307b7503e9764e737264ab7bc44240652394e374d DB_PASSWORD: 1000687e9b8472b98dc7395b09b7fdc628cdff5b6dc1e1a548ba6bffe76c30874f4b SECRET_KEY_BASE: 1000921032b2b78973963c1a2d0b6bc5ec98bfbde55cbd4c49c32ebaed73f8d6a736 mysql: secretEnvs: MYSQL_ROOT_PASSWORD: 1000f29b2bd6877c2f690d7fe82822a590bfd154db2d947102eb4341722cf564f635 minio: secretEnvs: MINIO_ROOT_USER: 1000ee40b96582f838ebbf4fb245ea15d912f0a1e76e3ba8461078a3078c06764315 MINIO_ROOT_PASSWORD: 1000b7467033b6227b7a2052ac60e16e34382f26dee78f4015276b01b1b5c25a887a

Чтобы увидеть расшифрованными секреты, хранящиеся в .helm/secret-values.yaml, можно выполнить следующую команду:

werf helm secret values decrypt .helm/secret-values.yaml

Ожидаемый результат:

app:
  secretEnvs:
    S3_USERNAME: minioadmin
    S3_PASSWORD: minioadmin
    DB_USERNAME: root
    DB_PASSWORD: password
    SECRET_KEY_BASE: something
mysql:
  secretEnvs:
    MYSQL_ROOT_PASSWORD: password
minio:
  secretEnvs:
    MINIO_ROOT_USER: minioadmin
    MINIO_ROOT_PASSWORD: minioadmin

Теперь нужно передать секреты из .helm/secret-values.yaml обратно в конфигурационный файл приложения. Для этого сначала передадим их в ресурс Secret:

apiVersion: v1
kind: Secret
metadata:
  name: app-envs
type: Opaque
data:
  {{- range $key, $val := .Values.app.secretEnvs }}
  "{{ $key }}": "{{ $val | b64enc }}"
  {{- end }}
apiVersion: v1 kind: Secret metadata: name: app-envs type: Opaque data: {{- range $key, $val := .Values.app.secretEnvs }} "{{ $key }}": "{{ $val | b64enc }}" {{- end }}

… а затем примонтируем Secret в контейнеры как набор переменных окружения:

...
containers:
- name: backend
  image: {{ .Values.werf.image.backend }}
  command: ["node", "./bin/www"]
  ports:
  - containerPort: 3000
  envFrom:
  - configMapRef:
      name: app-envs
  - secretRef:
      name: app-envs
... containers: - name: backend image: {{ .Values.werf.image.backend }} command: ["node", "./bin/www"] ports: - containerPort: 3000 envFrom: - configMapRef: name: app-envs - secretRef: name: app-envs

Изменения в остальных файлах .helm/templates для вынесения секретной конфигурации в Secret аналогичны, поэтому приводить здесь листинги не будем, на их содержимое можно посмотреть в репозитории.

После передачи переменных окружения в контейнер остаётся только подставить их в конфигурационных файлах приложения.

Чтобы единообразно управлять конфигурацией приложения, установим модуль config:

npm i config

Этот модуль позволит переопределять конфигурацию в зависимости от типа окружения, а также использовать переменные окружения. Это конфигурационный файл со значениями по умолчанию:

{
  "minio": {
    "region": "us-east-1",
    "endpoint": {
      "protocol": "http",
      "hostname": "minio",
      "port": 9000,
      "path": "/"
    },
    "bucket": "werf-guide-app",
    "forcePathStyle": true,
    "credentials": {
      "accessKeyId": "minioadmin",
      "secretAccessKey": "minioadmin"
    }
  }
}
{ "minio": { "region": "us-east-1", "endpoint": { "protocol": "http", "hostname": "minio", "port": 9000, "path": "/" }, "bucket": "werf-guide-app", "forcePathStyle": true, "credentials": { "accessKeyId": "minioadmin", "secretAccessKey": "minioadmin" } } }

Здесь задается переопределение конфигурации приложения через переменные окружения:

{
  "database": {
    "username": "DB_USERNAME",
    "password": "DB_PASSWORD"
  },
  "credentials": {
    "accessKeyId": "S3_USERNAME",
    "secretAccessKey": "S3_PASSWORD"
  }
}
{ "database": { "username": "DB_USERNAME", "password": "DB_PASSWORD" }, "credentials": { "accessKeyId": "S3_USERNAME", "secretAccessKey": "S3_PASSWORD" } }

К сожалению, sequelize нельзя использовать с модулем config, так как в конфиг первого определяются все окружения, а у config окружения делятся по файлам. Чтобы обойти это ограничение, можно переписать конфигурацию sequelize на JavaScript и подставить переменные окружения с помощью node:

// https://sequelize.org/master/manual/migrations.html#dynamic-configuration
module.exports = {
  development: {
    username: 'root',
    password: 'null',
    database: 'werf-guide-app',
    host: 'mysql',
    dialect: 'mysql',
  },
  test: {
    username: 'root',
    password: null,
    database: 'werf-guide-app',
    host: 'mysql',
    dialect: 'mysql',
  },
  production: {
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: 'werf-guide-app',
    host: 'mysql',
    dialect: 'mysql',
  },
};
// https://sequelize.org/master/manual/migrations.html#dynamic-configuration module.exports = { development: { username: 'root', password: 'null', database: 'werf-guide-app', host: 'mysql', dialect: 'mysql', }, test: { username: 'root', password: null, database: 'werf-guide-app', host: 'mysql', dialect: 'mysql', }, production: { username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: 'werf-guide-app', host: 'mysql', dialect: 'mysql', }, };

Для хранения и монтирования секретных конфигурационных файлов целиком также можно использовать Secret. Выглядит это аналогично использованию ConfigMap для монтирования несекретных файлов конфигурации, описанному выше, за тем лишь исключением, что само содержание Secret должно храниться зашифрованным в файлах .helm/secret/... или в .helm/secret-values.yaml. Подробнее об этом можно прочитать в документации werf.

Больше информации по работе с секретами также доступно в документации werf.

Проверка работоспособности

Убедимся, что изменения в конфигурации не повлияли на работоспособность приложения:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app

Ожидаемый результат:

┌ ⛵ image backend
│ ┌ Building stage backend/dockerfile
│ │ backend/dockerfile  Sending build context to Docker daemon  522.2kB
│ │ backend/dockerfile  Step 1/27 : FROM node:12-alpine as builder
    ...
│ │ backend/dockerfile  Successfully built 9a1b9b14d3f5
│ │ backend/dockerfile  Successfully tagged e889fc6d-3113-495c-9af3-dc0f2f21111f:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (14.56 seconds)
│ ├ Info
│ │      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:084bb9ae18181c10c2c54d9724f6af7906eedbf6fc7f9c1f52a57633-1637087610416
│ │        id: 9a1b9b14d3f5
│ │   created: 2022-11-16 21:33:30 +0000 UTC
│ │      size: 52.2 MiB
│ └ Building stage backend/dockerfile (21.26 seconds)
└ ⛵ image backend (28.11 seconds)

┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile  Sending build context to Docker daemon  522.2kB
│ │ frontend/dockerfile  Step 1/30 : FROM node:12-alpine as builder
    ...
│ │ frontend/dockerfile  Successfully built 30b79821ed1f
│ │ frontend/dockerfile  Successfully tagged 62ec4df5-2780-46e8-9243-21cbc5330f7a:latest
│ │ ┌ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app
│ │ └ Store stage into <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app (10.35 seconds)
│ ├ Info
│ │      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app:19eb45b2118f57b3fb0b994c60989b8f78c5f6697706cf34f3619469-1637087610274
│ │        id: 30b79821ed1f
│ │   created: 2022-11-16 21:33:30 +0000 UTC
│ │      size: 9.4 MiB
│ └ Building stage frontend/dockerfile (16.98 seconds)
└ ⛵ image frontend (23.39 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ ├── guide-app-64db57b499-fvhmd   0/2        0             Init:0/1            Waiting for: replicas 2->1                          ↵
│ │
│ │ └── guide-app-66b78c6dd8-49mxt   2/2        0             Running
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           0               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── 0                            0/1        0             ContainerCreating   Waiting for: ready 0->1, up-to-date 1->1            ↵
│ │
│ │ mysql                                                                         1/1           0               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── 0                            0/0        0             -                   Waiting for: ready 0->1, up-to-date 1->1
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev20                                                    1             2s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev20--1-mc64 0/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │     k
│ │ setup-minio-rev20                                                             1             2s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev20--1-r4g4r         0/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
│ ┌ deploy/werf-guide-app po/werf-guide-app-64db57b499-fvhmd container/wait-db-readiness logs
│ │
│ │ Sequelize CLI [Node: 12.22.7, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.js".
│ │ Using environment "production".
│ │ up 20211101064002-create-talker.js
│ └ deploy/werf-guide-app po/werf-guide-app-64db57b499-fvhmd container/wait-db-readiness logs
│
│ ┌ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│
│ ┌ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ ├── guide-app-64db57b499-fvhmd   0/2        0             Init:0/1 ->         Waiting for: replicas 2->1                          ↵
│ │
│ │ │                                                         PodInitializing
│ │ └── guide-app-66b78c6dd8-49mxt   2/2        0             Running
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           0               1
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── 0                            0/1        0             ContainerCreating   Waiting for: ready 0->1, up-to-date 1->1            ↵
│ │
│ │ mysql                                                                         1/1           0->1            1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev20                                                    1             9s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev20--1-mc64 1/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │     k                                                     -> Running
│ │ setup-minio-rev20                                                             1             7s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev20--1-r4g4r         1/1        0             ContainerCreating   Waiting for: pods should be terminated, succeeded 0->1
│ │                                                           -> Running
│ └ Status progress
│
│ ┌ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│
│ ┌ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│
│ ┌ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│ │ curl: (7) Failed to connect to minio port 9000: Connection refused
│ └ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│
│ ┌ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ └ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                2->1/1        1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ ├── guide-app-64db57b499-fvhmd   2/2        0             PodInitializing ->
│ │ │                                                         Running
│ │ └── guide-app-66b78c6dd8-49mxt   2/2        0             Running ->
│ │                                                           Terminating
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           0->1            1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             ContainerCreating
│ │                                                           -> Running
│ │ mysql                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev20                                                    1             9s              0/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev20--1-mc64 1/1        0             Running             Waiting for: pods should be terminated, succeeded 0->1
│ │     k
│ │ setup-minio-rev20                                                             1             7s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev20--1-r4g4r         1/1        0             Running             Waiting for: pods should be terminated, succeeded 0->1
│ └ Status progress
│
│ ┌ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │ mysqladmin: connect to server at 'mysql' failed
│ │ error: 'Access denied for user 'root'@'172.17.0.1' (using password: YES)'
│ │
│ │ Sequelize CLI [Node: 12.22.7, CLI: 6.3.0, ORM: 6.9.0]
│ │
│ │ Loaded configuration file "config/database.js".
│ │ Using environment "production".
│ │ No migrations were executed, database schema was already up to date.
│ └ job/setup-and-migrate-db-rev20 po/setup-and-migrate-db-rev20--1-mc64k container/setup-and-migrate-db logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ ├── guide-app-64db57b499-fvhmd   2/2        0             Running
│ │ └── guide-app-66b78c6dd8-49mxt   2/2        0             Terminating
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ mysql                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev20                                                    0             20s             0->1/0                ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── and-migrate-db-rev20--1-mc64 0/1        0             Running ->          Waiting for: pods should be terminated
│ │     k                                                     Completed
│ │ setup-minio-rev20                                                             1             7s              0/0
│ │ │   POD                          READY      RESTARTS      STATUS              ---
│ │ └── minio-rev20--1-r4g4r         1/1        0             Running             Waiting for: pods should be terminated, succeeded    ↵
│ │ 0->1
│ └ Status progress
│
│ ┌ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│ │ Added `minio` successfully.
│ │ Bucket created successfully `minio/werf-guide-app`.
│ └ job/setup-minio-rev20 po/setup-minio-rev20--1-r4g4r container/setup-minio logs
│
│ ┌ Status progress
│ │ DEPLOYMENT                                                                    REPLICAS      AVAILABLE       UP-TO-DATE
│ │ werf-guide-app                                                                1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ ├── guide-app-64db57b499-fvhmd   2/2        0             Running
│ │ └── guide-app-66b78c6dd8-49mxt   2/2        0             Terminating
│ │ STATEFULSET                                                                   REPLICAS      READY           UP-TO-DATE
│ │ minio                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ mysql                                                                         1/1           1               1
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── 0                            1/1        0             Running
│ │ JOB                                                                           ACTIVE        DURATION        SUCCEEDED/FAILED
│ │ setup-and-migrate-db-rev20                                                    0             20s             1/0                   ↵
│ │
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── and-migrate-db-rev20--1-mc64 0/1        0             Completed
│ │     k
│ │ setup-minio-rev20                                                             0             24s             0->1/0
│ │ │   POD                          READY      RESTARTS      STATUS
│ │ └── minio-rev20--1-r4g4r         0/1        0             Running ->
│ │                                                           Completed
│ └ Status progress
└ Waiting for release resources to become ready (24.64 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Tue Nov 16 21:33:50 2022
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 20
TEST SUITE: None
Running time 59.06 seconds

Проверим доступность приложения:

curl http://werf-guide-app.test/ping

Ожидаемый результат:

pong

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

назад
далее