Запускаем WordPress c MySQL в Kubernetes

Как для приложения в Kubernetes обеспечить надёжное хранение данных
12 минут

Едва ли в мире есть более популярный движок для создания блогов и небольших сайтов, чем WordPress — около 26% всех веб-сайтов в интернете созданы на его основе. Многие хостинги включают его в список своих сервисов, а официальный docker-образ был скачан более 10 миллионов раз и является одним из 25 самых популярных образов на Docker Hub. Запустив его не просто в докере, а в Kubernetes, мы можем создать масштабируемую платформу для блога или веб-сайта: при росте нагрузки мы просто масштабируем количество реплик приложения.


Планируем архитектуру

Теория: stateless- и stateful-приложения

Перед тем как приступить к установке WordPress, нам придется вкратце рассмотреть несколько базовых понятий, связанных с хранением данных в облаках и в Kubernetes.

Stateless-приложения. Для примера возьмем два приложения — «корзину» в интернет-магазине и наш блог на WordPress. Когда мы добавляем товары в корзину (но не переходим к оплате заказа), она сохраняет все данные в cookies на нашей стороне. По завершении сессии приложение сбрасывает все данные и не хранит ничего на сервере. Когда мы снова заходим в интернет-магазин, информация о товарах считывается из нашей системы и снова отображается в корзине. Чтобы работать с покупателем, корзине не нужно получать никакую информацию с бэкенда — роль хранилища выполняет система покупателя. Такие приложения называются stateless, поскольку они не хранят информацию о своем состоянии.

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

Как будем хранить данные?

Kubernetes поддерживает оба типа приложений, но принцип их запуска различен. Stateless-приложения не требуют выделения хранилища, но наш блог — stateful, и для него нужно выделить директорию для записи и чтения данных о состоянии. В нашей инсталляции WordPress мы будем хранить данные приложения в базе данных, конкретно — в MySQL. Для запуска WordPress и MySQL потребуется выделить две директории: одну для системных данных WordPress, другую для данных сайта и информации о пользователях, хранимых в MySQL.

С этим разобрались. Но где хранить все эти данные? Рассмотрим два варианта.

  • WordPress и MySQL могут хранить всю информацию внутри контейнеров, в которых они запущены. Это обеспечит сохранность данных на какое-то время, но при перезапуске пода (pod) все данные будут утеряны (по эффекту перезапустить под — это всё равно что удалить все данные на сервере). Этот вариант подходит, если вы хотите быстро протестировать работу приложений, но его точно не стоит использовать для работы в production.
  • Можно хранить данные приложений вне контейнера, используя внешнее хранилище на сервере. В этом случае перезапуск пода никак не повлияет на сохранность данных и, кроме того, мы сможем делать бэкапы содержимого. Именно этот вариант мы и выберем.

Что сделать, чтобы использовать внешнее хранилище?

Чтобы работать с хранилищем в Kubernetes и, в частности, смонтировать в него WordPress и MySQL, нам необходимо сделать две вещи:

  1. настроить утилиту для управления хранилищем на сервере хранилища,
  2. обеспечить взаимодействие между этой утилитой и приложениями в Kubernetes.

Kubernetes поддерживает множество утилит для работы с хранилищем: Ceph, GlusterFS, iSCSI, NFS и др. Список всех поддерживаемых типов можно найти в официальной документации Kubernetes. Для простоты в данной статье мы будем использовать NFS.

Для работы с хранилищем Kubernetes использует абстракции PersistentVolume и PersistentVolumeClaim. Что они из себя представляют? Объект PersistentVolume (PV) управляет постоянным хранилищем в кластере. Он поддерживает различные типы внешнего хранилища, например NFS, GlusterFS, Ceph и многие другие. PersistentVolumeClaim (PVC) — это запрос на использование хранилища для конкретного приложения. В упрощенном виде их взаимодействие можно изобразить следующим образом: приложение обращается к PersistentVolumeClaim, который запрашивает хранилище у PersistentVolume, который, в свою очередь, монтирует данные приложений в директорию на сервере хранилища. Чтобы не создавать PersistentVolume вручную, мы установим nfs-client-provisioner — приложение, которое автоматически создает PV по требованию PVC.

Как поды обращаются к физическому хранилищу

Схема обращения подов к физическому хранилищу

Для работы подов с хранилищами в архитектуре Kubernetes выделена абстракция Volume. Под обращается к нему как хранилищу без учёта того, как это хранилище реализовано. С помощью volume решается две проблемы:

  • сохранение данных при падении контейнера (пода),
  • возможность для двух и более подов работать с общими файлами.

PersistentVolume (PV) — это Volume для работы подов с физическим хранилищем через API. PV представляет собой объект в Kubernetes, который содержит в себе информацию, как монтировать физическое хранилище и его метаданные.

PersistentVolumeClaim (PVC) — это запрос на создание PV и предоставление Storage.


План развёртывания

В процессе подготовки NFS и установки WordPress и MySQL мы выполним следующие шаги:

  1. Установка утилиты NFS для управления хранилищем.
  2. Установка nfs-client-provisioner для автоматического выделения хранилища в Kubernetes.
  3. Создание PersistentVolumeClaim для запроса места в хранилище.
  4. Создание Secret для MySQL для доступа к MySQL.
  5. Запуск MySQL.
  6. Запуск WordPress.

Подготовка NFS

Для работы с NFS необходимо настроить NFS-сервер на отдельной машине и NFS-клиенты на машинах с Kubernetes.

Зайдем на машину-сервер, которую будем использовать для хранения данных, установим на нее файервол и откроем порты TCP:111, UDP: 111, TCP:2049, UDP:2049. В нашем примере мы используем CenOS 7.5. Для этого выполним команды: sudo yum install firewalld -y
sudo systemctl start firewalld
sudo systemctl enable firewalld
sudo firewall-cmd --zone=public --add-port=111/tcp --permanent
sudo firewall-cmd --zone=public --add-port=111/udp --permanent
sudo firewall-cmd --zone=public --add-port=2049/tcp --permanent
sudo firewall-cmd --zone=public --add-port=2049/udp --permanent
sudo firewall-cmd --reload

Помимо этого, не забудьте дополнительно защитить внутренние ресурсы от доступа из интернета. Для этого закройте доступ к портам извне на уровне фаервола сети, в которой развёрнута ваша инсталляция.

После этого установим NFS-utils для работы с хранилищем: sudo yum install nfs-utils

Создадим директорию для монтирования данных на диск (/nfs) и запустим nfs-server: sudo mkdir -p /nfs
sudo chmod -R 777 /nfs
sudo systemctl enable rpcbind
sudo systemctl enable nfs-server
sudo systemctl enable nfs-lock
sudo systemctl enable nfs-idmap
sudo systemctl start rpcbind
sudo systemctl start nfs-server
sudo systemctl start nfs-lock
sudo systemctl start nfs-idmap

Теперь добавим директорию NFS в частную подсеть Kubernetes: sudo vi /etc/exports

Добавим следующую строку в файл и сохраним его:
/nfs X.X.X.X/X(rw,sync,no_root_squash) где X.X.X.X/X — адрес подсети, например — 172.31.32.0/24.

Теперь применим конфигурацию, выполнив команду:
exportfs -a

Перезапустим nfs: sudo systemctl restart nfs-server

Зайдем на остальные машины, установим утилиты для работы с nfs и запустим сервисы: sudo yum install nfs-utils
systemctl enable rpcbind
systemctl enable nfs-server
systemctl enable nfs-lock
systemctl enable nfs-idmap
systemctl start rpcbind
systemctl start nfs-server
systemctl start nfs-lock
systemctl start nfs-idmap

Готово!


Установка nfs-client-provisioner

Чтобы не монтировать PersistentVolume к хранилищу вручную для каждого PVC, установим nfs-client provisioner на каждой машине-клиенте. Для установки будем использовать Helm. Выполним команду:
helm install --set nfs.server=Y.Y.Y.Y --set nfs.path=/nfs stable/nfs-client-provisioner где Y.Y.Y.Y — внутренний адрес NFS-сервера, например — 172.31.32.1.

Сделаем nfs типом хранилища (StorageClass) по умолчанию: kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'


Создаем PersistentVolumeClaim

Чтобы хранить данные WordPress и MySQL в NFS, необходимо запросить часть хранилища (в данном случае по 1 GiB) для каждого приложения через PVC. Благодаря запущенному nfs-provisioner PV будут предоставлены автоматически. Создадим файлы pvc-wordpress.yaml и pvc-mysql.yaml со следующим содержанием:

pvc-wordpress.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

и pvc-mysql.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim-db-wordpress-one
  labels:
    app: wordpress-one
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

Теперь создадим объекты PVC: kubectl create -f pvc-wordpress.yaml
kubectl create -f pvc-mysql.yaml

Готово, теперь можно переходить к запуску MySQL и WordPress.


Создаем secret для MySQL

Secret — объект, который хранит какую-либо конфиденциальную информацию типа паролей или ключей. Данные, хранимые в Secret, должны быть закодированы по стандарту base64. Сейчас мы создадим секрет c паролем для пользователя admin в MySQL, зададим создание случайного пароля: openssl rand -base64 32 | base64

На выходе получим зашифрованный пароль: QlFhZzlEOWF6c3JoMTU4Rjh2U3FDVUdHNE9KSm4xMVBtVDV1Rno1Szkvbz0K

Теперь нужно создать secret.yaml для MySQL и WordPress, на который будет ссылаться MYSQL_ROOT_PASSWORD в environment деплойментов:

apiVersion: v1
kind: Secret
metadata:
  name: mysql-pass
type: Opaque
data:
  password: QlFhZzlEOWF6c3JoMTU4Rjh2U3FDVUdHNE9KSm4xMVBtVDV1Rno1Szkvbz0K

Выполним команду для создания секрета из файла: kubectl create -f secret.yaml


Запускаем MySQL

Теперь создадим mysql-deploy.yaml, в котором опишем параметры запуска деплоймента и сервиса для MySQL:

apiVersion: v1
kind: Service
metadata:
  name: wordpress-db-one
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-db-one
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:5.7
        name: mysql
        args:
        - "--ignore-db-dir=lost+found"
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: db-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: db-storage
        persistentVolumeClaim:
          claimName: claim-db-wordpress-one

Файл состоит из 2 файлов конфигураций: Service открывает порт 3306 для всех контейнеров, у которых прописаны лейблы app:wordpress и tier:mysql. Часть Deployment описывает параметры создания деплоймента и specs контейнера с MySQL:

  • используется образ mysql:5.7
  • указываются лейблы app:wordpress и tier:frontend, прописанные в Service для WordPress.
  • в качестве переменной MYSQL_ROOT_PASSWORD используется password из созданного секрета.
  • открывается порт 3306
  • Директория /var/lib/mysql внутри контейнера монтируется в Volume через PVC с названием claim-db-wordpress-one.

Создадим деплоймент и сервис из файла: kubectl create -f mysql-deploy.yaml


Запускаем WordPress

Процесс запуска WordPress аналогичен описанному выше. Создадим wordpress-deploy.yaml:

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:4.8-apache # latest: wordpress:4.9-php7.2-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-db-one # This one is from Service
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wp-pv-claim

Как и в предыдущем случае, файл состоит из 2 частей. Service пробрасывает 80-й порт контейнера на внешний IP:порт машины для всех контейнеров с лейблами app:wordpress и tier:frontend. Deployment содержит следующие specs контейнера WordPress:

  • запускаемый образ с DockerHub - wordpress:4.8-apache
  • указаны лейблы app:wordpress и tier:frontend, используемые в Service.
  • указаны переменные WORDPRESS_DB_HOST (внутренний hostname MySQL) и MYSQL_ROOT_PASSWORD, использующий значение из созданного секрета
  • открыт 80-й порт.
  • Директория /var/www/html внутри контейнера монтируется в Volume через PVC с названием wp-pv-claim.

Создадим Deployment и Service из файла wordpress-deploy.yaml: kubectl create -f wordpress-deploy.yaml

Убедимся, что все работает: kubectl get po

Результат выполнения команды kubectl get po

Откроем WordPress

Теперь откроем WordPress в браузере. Для этого получим порт приложения, выполнив команду kubectl get svc:

Результат выполнения команды kubectl get svc

Перейдем по адресу: внешний_IP:порт_сервиса (в нашем случае 32095).

Экран выбора языка в WordPress

Перед нами появился экран выбора языка — WordPress готов к работе.


Проверка

Теперь убедимся, что мы верно настроили работу WordPress и MySQL с хранилищем. В панели управления вордпресса укажем название блога («WordPress в Kubernetes») и перейдем на главную страницу по адресу внешний IP:порт_сервиса. На странице появилось название блога.

Блог на WordPress

Теперь перезапустим поды WordPress и MySQL и проверим, сохранились ли данные приложений. kubectl get po

Результат выполнения команды kubectl get po

Удалим поды и подождем, пока будут созданы новые: kubectl delete po название_пода1 название_пода2

Результат выполнения команды kubectl delete po

Снова зайдем на главную страницу WordPress.

Блог на WordPress
Картинка та же, так что при удалении и восстановлении пода всё сохранилось

После перезапуска подов оба приложения взяли сохраненные данные из NFS-хранилища — значит, мы все настроили правильно!


Заключение

С Kubernetes мы можем быстро развернуть WordPress с MySQL и смонтировать их данные в постоянное хранилище. Мы использовали NFS из-за простоты настройки, однако для более надежного production рекомендуется использовать более сложные распределенные системы хранения, например GlusterFS или Ceph.

Тем не менее, NFS отлично подходит для небольших проектов. Если в какой-то момент что-то пойдет не так и WordPress или MySQL «упадет» или перезапустится, мы не потеряем контент благодаря тому, что они хранятся не внутри контейнеров, а во внешнем хранилище. Главное — не забывать регулярно делать бэкапы.