Подключаем внешний PostgreSQL

Прямо сейчас мы пишем новые главы самоучителя!
Эта страница находится в разработке.

Показать, что уже готово

Примеры ниже не предназначены для использования в 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//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

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

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rfT ~/werf-guide/guides/ .
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/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/ ~/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//* .
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/werf-guides ~/werf-guide/guides

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

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

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

Файлы, упомянутые в главе

  • .helm/templates/deployment.yaml
  • .helm/templates/job-migrations.yaml
  • migrations/1588019669425_001-labels.js
  • app.js
  • package.json

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

В качестве базы данных возьмём PostgreSQL. Мы предполагаем, что база данных уже где-то реализована: вы либо подняли её на отдельном сервере, либо воспользовались Managed сервисом у своего cloud provider-а. Вопросы, связанные с самостоятельной установкой БД в кластер мы разберём в главе “Работа с инфраструктурой”

А что если БД в кластере?

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

Размещение базы данных в кластере вручную — комплексный вопрос, требующий организации хранилища для данных, реквизитов доступа и т.п. Для того, чтобы всё сделать корректно требуются продвинутые навыки.

Подключение приложения к базе PostgreSQL

Наше javascript-приложение мы перепишем, оставив то же API, но заменив место хранения данных: вместо sqlite теперь будет использоваться PostgreSQL. Соответственно, изменятся и переменные окружения, которыми будет конфигурироваться приложение.

Мы не будем приводить полный код приложения — его можно посмотреть в github.

Для подключения Node.js-приложения к PostgreSQL необходимо установить npm-пакет pg и сконфигурировать:

// Connection to Redis
const Pool = require('pg').Pool
let client;
try {
  client = new Pool({
    user: process.env.POSTGRESQL_USER,
    host: process.env.POSTGRESQL_HOST,
    database: process.env.POSTGRESQL_DB,
    password: process.env.POSTGRESQL_PASSWORD,
    port: process.env.POSTGRESQL_PORT,
  })
} catch (err) {
  console.error('connection to PSQL failed')
  console.error(err);
  process.exit(1);
}
// Connection to Redis const Pool = require('pg').Pool let client; try { client = new Pool({ user: process.env.POSTGRESQL_USER, host: process.env.POSTGRESQL_HOST, database: process.env.POSTGRESQL_DB, password: process.env.POSTGRESQL_PASSWORD, port: process.env.POSTGRESQL_PORT, }) } catch (err) { console.error('connection to PSQL failed') console.error(err); process.exit(1); }

Далее, внесём изменения в объект Deployment: пропишем новые переменные окружения и реализуем initContainer, который не даст запустить приложение, пока база данных не запустится.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: basicapp
spec:
  selector:
    matchLabels:
      app: basicapp
  revisionHistoryLimit: 3
  strategy:
    type: RollingUpdate
  replicas: 1
  template:
    metadata:
      labels:
        app: basicapp
    spec:
      imagePullSecrets:
      - name: "registrysecret"
      initContainers:
      - name: wait-postgres
        image: "foobic/pg_isready:latest"
        command: ['/scripts/pg_isready.sh']
        env:
        - name: HOST
          value: "host.docker.internal"
        - name: PORT
          value: "5432"
        - name: DBNAME
          value: "postgres"
        - name: RETRIES
          value: "-1"
      containers:
      - name: basicapp
        command: ["node","/app/app.js"]
        image: {{ .Values.werf.image.basicapp }}
        workingDir: /app
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
          - name: "POSTGRESQL_HOST"
            value: "host.docker.internal"
          - name: "POSTGRESQL_PORT"
            value: "5432"
          - name: "POSTGRESQL_DB"
            value: "postgres"
          - name: "POSTGRESQL_USER"
            value: "postgres"
          - name: "POSTGRESQL_PASSWORD"
            value: "mysecretpassword"
apiVersion: apps/v1 kind: Deployment metadata: name: basicapp spec: selector: matchLabels: app: basicapp revisionHistoryLimit: 3 strategy: type: RollingUpdate replicas: 1 template: metadata: labels: app: basicapp spec: imagePullSecrets: - name: "registrysecret" initContainers: - name: wait-postgres image: "foobic/pg_isready:latest" command: ['/scripts/pg_isready.sh'] env: - name: HOST value: "host.docker.internal" - name: PORT value: "5432" - name: DBNAME value: "postgres" - name: RETRIES value: "-1" containers: - name: basicapp command: ["node","/app/app.js"] image: {{ .Values.werf.image.basicapp }} workingDir: /app ports: - containerPort: 3000 protocol: TCP env: - name: "POSTGRESQL_HOST" value: "host.docker.internal" - name: "POSTGRESQL_PORT" value: "5432" - name: "POSTGRESQL_DB" value: "postgres" - name: "POSTGRESQL_USER" value: "postgres" - name: "POSTGRESQL_PASSWORD" value: "mysecretpassword"
Что здесь написано?

Init Container-ы запускаются перед основными контейнерами. Пока они не завершатся успешно — основные контейнеры не будут запущены.

Мы используем образ foobic/pg_isready, который представляет из себя alpine с консольной утилитой pg_isready, позволяющей проверять готовность базы Postgre. Он будет пытаться подсоединиться к базе раз за разом.

В нашем примере в качестве HOST используется значение host.docker.internal. Это значение необходимо, если вы ведёте разработку в локальном kubernetes (например, minikube), а базу данных развернули прямо на хост-машине. См. подробнее документацию Docker о том, как из контейнера добраться до хост-машины.

Соответственно, аналогично прописываем переменные окружения у контейнера basicapp.

Оставлять ключи подключения в helm-чарте — плохая практика. Чтобы иметь возможность конфигурировать разные значения для разных стендов (тестового, продакшн и т.п.) и обезопасить себя — воспользуйтесь подходами, описанными в главе “Конфигурирование инфраструктуры в виде кода”.

Выполнение миграций

Работа реальных приложений почти немыслима без выполнения миграций. С точки зрения Kubernetes миграции выполняются созданием объекта Job, который разово запускает Pod с необходимыми контейнерами. Запуск миграций мы пропишем после каждого деплоя приложения.

Для выполнения миграций в БД нами будет использоваться пакет node-pg-migrate. Установим его: npm install node-pg-migrate.

Как настраиваем node-pg-migrate?

Запуск миграции поместим в package.json, чтобы его можно было вызывать его через npm run migrate:

   "migrate": "node-pg-migrate"

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

node
├── migrations
│   ├── 1588019669425_001-users.js
│   └── 1588172704904_add-avatar-status.js
├── src
├── package.json
...

Далее необходимо добавить запуск миграций непосредственно в Kubernetes.

Как уже упоминалось, для этого будет создан ресурс Job в Kubernetes. Он обеспечивает единоразовый запуск Pod’а с необходимыми нам контейнерами; его предназначение — выполнение конечной функции, после чего он завершится. Повторно он может быть запущен только при последующих деплоях.

Как конфигурируем сам Job?

Подробнее о конфигурировании объекта Job можно почитать в документации Kubernetes.

Также мы воспользуемся аннотациями Helm helm.sh/hook и helm.sh/weight, чтобы Job выполнялся после того, как применится новая конфигурация.

    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/weight": "5"
"helm.sh/hook": post-install,post-upgrade "helm.sh/weight": "5"

Вопросы по настройке Job могут возникнуть уже на этапе, когда мы видим блок annotations. Он содержит в себе настройки для Helm, которые определяют, когда именно нужно запускать Job (подробнее про них можно узнать в документации Helm).

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

А вторая аннотация отвечает за порядок запуска этого Job. Например, если мы имеем в нашем чарте несколько Job разного назначения и не хотим запускать их единовременно, а только по порядку, можно указать для них веса.

По аналогии с Deployment мы будем использовать initContainer.

apiVersion: batch/v1
kind: Job
metadata:
  name: basicapp-migrations
  annotations:
    "helm.sh/hook": post-install, post-upgrade
    "helm.sh/weight": "5"
spec:
  activeDeadlineSeconds: 600
  template:
    metadata:
      name: basicapp-migrations
    spec:
      imagePullSecrets:
        - name: "registrysecret"
      restartPolicy: OnFailure
      initContainers:
        - name: wait-postgres
          image: "foobic/pg_isready:latest"
          command: ['/scripts/pg_isready.sh']
          env:
            - name: HOST
              value: "host.docker.internal"
            - name: PORT
              value: "5432"
            - name: DBNAME
              value: "postgres"
            - name: RETRIES
              value: "-1"
      containers:
        - name: init-tables
          image: {{ .Values.werf.image.basicapp }}
          command: ['node']
          args: ['node_modules/node-pg-migrate/bin/node-pg-migrate', 'up']
          workingDir: /app
          env:
            - name: "DATABASE_URL"
              value: "postgres://postgres:mysecretpassword@host.docker.internal:5432/postgres"
apiVersion: batch/v1 kind: Job metadata: name: basicapp-migrations annotations: "helm.sh/hook": post-install, post-upgrade "helm.sh/weight": "5" spec: activeDeadlineSeconds: 600 template: metadata: name: basicapp-migrations spec: imagePullSecrets: - name: "registrysecret" restartPolicy: OnFailure initContainers: - name: wait-postgres image: "foobic/pg_isready:latest" command: ['/scripts/pg_isready.sh'] env: - name: HOST value: "host.docker.internal" - name: PORT value: "5432" - name: DBNAME value: "postgres" - name: RETRIES value: "-1" containers: - name: init-tables image: {{ .Values.werf.image.basicapp }} command: ['node'] args: ['node_modules/node-pg-migrate/bin/node-pg-migrate', 'up'] workingDir: /app env: - name: "DATABASE_URL" value: "postgres://postgres:mysecretpassword@host.docker.internal:5432/postgres"

Деплой и тестирование

После того, как изменения в код внесены — нужно закоммитить изменения в git, задеплоить в кластер с помощью werf converge и протестировать методы /api/labels. Если миграции не отработают — ни один из методов не будет работать, т.к. не будет хватать таблицы.