MySQL Deployment в Kubernetes на одном хосте
Опубликовано:
Используемые термины: MySQL, Kubernetes.
В инструкции разберем, как развернуть MySQL в Kubernetes без StatefulSet и без оператора — через обычный Deployment с одной репликой и отдельным PVC для данных. Пройдем весь путь: подготовим namespace, зададим пароль root через Secret, настроим параметры сервера через ConfigMap, поднимем том для данных и опубликуем сервис.
Создание нового контекста и получим информацию о StorageClass
Манифест для root пароля
Конфигурационный файл ConfigMap
PVC для хранения данных
Сервис для доступа к MySQL
Deployment-манифест
Проверка запуска пода и сервиса
Скрипт для создания базы и пользователя
Решение возможных проблем
Пример создания резервной копии
Создание Namespace и определение StorageClass
Все ресурсы вынесем в отдельный namespace, а манифесты будем хранить в отдельном каталоге на диске, чтобы не путать их с другими проектами.
mkdir -p /opt/mysql && cd /opt/mysql
kubectl create namespace database
* все манифесты — в /opt/mysql, ресурсы — в namespace database.
Перед созданием PVC нужно знать, какой StorageClass доступен в кластере — без него том не сможет получить хранилище.
kubectl get storageclass
* запомните имя класса (часто local-path, standard, hostpath) — оно понадобится в PVC.
Пароль root через Secret
Официальный образ MySQL требует пароль root при первом старте, когда каталог данных еще пустой. Хранить его прямо в манифесте Deployment небезопасно, поэтому пароль выносим в отдельный Secret и подключаем его переменной окружения.
vi mysql-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: mysql-single-creds
namespace: database
type: Opaque
stringData:
MYSQL_ROOT_PASSWORD: ChangeMe_StrongPass
* где:
- stringData — kubectl сам закодирует в base64; пароль не попадает в историю shell (в отличие от --from-literal).
- MYSQL_ROOT_PASSWORD — пароль суперпользователя root; официальный образ требует его при первом старте (пустой data-каталог). БД и роль приложения на этом шаге не создаем.
- mysql-secret.yml — после apply удаляем файл с диска.
Применяем манифест и сразу удалим его:
kubectl apply -f mysql-secret.yml
shred -u mysql-secret.yml
Конфигурационный файл ConfigMap
Кодировку, часовой пояс и лимиты MySQL зададим не через правку конфига внутри контейнера, а через ConfigMap — так настройки переживут пересоздание пода и останутся в манифестах.
vi mysql-config.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-single-config
namespace: database
data:
custom.cnf: |
[mysqld]
bind-address = 0.0.0.0
port = 3306
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-time-zone = '+03:00'
max_connections = 100
innodb_buffer_pool_size = 128M
skip-name-resolve
* где:
- custom.cnf — файл попадет в /etc/mysql/conf.d/; официальный образ подхватывает все *.cnf оттуда.
- innodb_buffer_pool_size = 128M — запас под limits.memory 512Mi в Deployment ниже; при смене memory меняйте и этот параметр (ориентир — до ~50% RAM контейнера).
- default-time-zone = '+03:00' — московское время через смещение; таблицы таймзон MySQL не нужны.
- skip-name-resolve — не резолвить DNS клиентов; ускоряет подключения из pod CIDR.
- bind-address = 0.0.0.0 — слушать все интерфейсы пода (иначе ClusterIP с других подов не достучится).
Применяем манифест:
kubectl apply -f mysql-config.yml
PVC для хранения данных
Данные MySQL должны пережить пересоздание пода, поэтому выносим их на отдельный PersistentVolumeClaim. Режим ReadWriteOnce означает, что том можно примонтировать только на одной ноде одновременно — это и определяет выбор стратегии обновления Deployment в следующем разделе.
vi mysql-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-single-data
namespace: database
labels:
app.kubernetes.io/name: mysql-single
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
* где:
- mysql-single-data — отдельный PVC (не volumeClaimTemplates StatefulSet); том живет независимо от пода Deployment.
- ReadWriteOnce — том на одной ноде; поэтому ниже стратегия Recreate.
- storageClassName — класс вашей одноузловой среды.
kubectl apply -f mysql-pvc.yml
Сервис для доступа к MySQL
Чтобы приложения в кластере обращались к базе по стабильному имени, а не по IP пода, публикуем MySQL через ClusterIP Service:
vi mysql-service.yml
apiVersion: v1
kind: Service
metadata:
name: mysql-single
namespace: database
labels:
app.kubernetes.io/name: mysql-single
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: mysql-single
ports:
- name: mysql
port: 3306
targetPort: mysql
* где:
- mysql-single — DNS для приложений: mysql-single.database.svc:3306.
- Headless Service не нужен — стабильное имя пода StatefulSet здесь не используем; клиентам достаточно ClusterIP.
Применяем настройку:
kubectl apply -f mysql-service.yml
Пример Deployment манифеста
Основной манифест. Он поднимает под с MySQL, монтирует PVC и ConfigMap, задает лимиты по памяти и CPU, а также три пробы (startup, liveness, readiness), которые следят за состоянием сервера и не пускают трафик, пока MySQL не готов принимать подключения. Стратегия Recreate здесь обязательна — том ReadWriteOnce не позволит новому поду примонтироваться, пока старый жив.
Создаем манифест:
vi mysql-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-single
namespace: database
labels:
app.kubernetes.io/name: mysql-single
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: mysql-single
template:
metadata:
labels:
app.kubernetes.io/name: mysql-single
spec:
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: mysql
image: mysql:8.4
imagePullPolicy: IfNotPresent
ports:
- name: mysql
containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-single-creds
key: MYSQL_ROOT_PASSWORD
resources:
requests:
memory: 512Mi
cpu: "250m"
limits:
memory: 512Mi
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "mysqladmin shutdown -uroot -p\"$MYSQL_ROOT_PASSWORD\" || true; sleep 10"]
startupProbe:
exec:
command: ["/bin/sh", "-c", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$MYSQL_ROOT_PASSWORD\""]
periodSeconds: 5
failureThreshold: 30
timeoutSeconds: 5
livenessProbe:
exec:
command: ["/bin/sh", "-c", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$MYSQL_ROOT_PASSWORD\""]
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command: ["/bin/sh", "-c", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$MYSQL_ROOT_PASSWORD\""]
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
privileged: false
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: config
mountPath: /etc/mysql/conf.d
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: mysql-single-data
- name: config
configMap:
name: mysql-single-config
* где:
- replicas: 1 — однохостовый инстанс; Deployment не делает репликацию MySQL сам по себе. Anti-affinity при одной реплике не нужен.
- strategy.type: Recreate — сначала гасим старый под, потом поднимаем новый. При RollingUpdate + RWO PVC новый под получит Multi-Attach / FailedMount, пока том занят старым.
- terminationGracePeriodSeconds: 60 — запас под preStop (shutdown MySQL + sleep 10) до SIGKILL.
- preStop — корректный mysqladmin shutdown, затем sleep 10, чтобы endpoint успел уйти из Service.
- MYSQL_ROOT_PASSWORD — единственная переменная инициализации; без MYSQL_USER/MYSQL_DATABASE поднимается только root и системные схемы.
- startupProbe / livenessProbe / readinessProbe — mysqladmin ping с паролем root из env; долгий первый init не убивает liveness; не Ready — нет трафика в ClusterIP.
- resources — memory requests = limits (Guaranteed QoS). CPU limits не задаем — только requests.
- runAsUser/fsGroup: 999 — UID пользователя mysql в официальном образе.
- readOnlyRootFilesystem не включаем: entrypoint и runtime MySQL пишут во временные пути образа.
- claimName: mysql-single-data — должен совпадать с PVC выше.
Применяем его к кубернетису:
kubectl apply -f mysql-deployment.yml
Проверка запуска пода и сервиса
После применения манифеста дождемся, пока под перейдет в Running, и убедимся, что PVC привязан, а сервис создан:
kubectl -n database rollout status deployment/mysql-single
kubectl get pods -n database -l app.kubernetes.io/name=mysql-single -o wide
* pod в Deployment должен быть в состоянии Running / Ready 1/1.
kubectl get pvc -n database
* mysql-single-data в статусе Bound.
kubectl get svc -n database
Подключимся к серверу изнутри пода и проверим, что root-доступ работает.
kubectl exec -n database deploy/mysql-single -- sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT USER(), CURRENT_USER(); SHOW DATABASES;"'
* пароль берется из env контейнера (sh -c, чтобы переменная раскрылась внутри пода). Пока должны быть только системные схемы (mysql, sys, performance_schema, information_schema).
База и пользователь для приложения
Root оставляем для администрирования, а для приложения заводим отдельную базу и роль с ограниченными правами только на нее. Схему и пользователя создаем SQL-скриптом после старта сервера, а не через переменные окружения образа:
vi init-app.sql
CREATE DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app'@'%' IDENTIFIED BY 'PasswordText';
GRANT ALL PRIVILEGES ON app.* TO 'app'@'%';
FLUSH PRIVILEGES;
* где:
- CREATE DATABASE / CREATE USER — схема и роль приложения после старта сервера, не через env образа.
- 'app'@'%' — доступ с любого хоста в кластере (pod CIDR через Service).
- FLUSH PRIVILEGES — применить изменения grant-таблиц сразу.
Для применения скрипта и создания базы с пользователем вводим команду:
cat init-app.sql | kubectl exec -i -n database deploy/mysql-single -- sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD"'
После скрипт лучше удалить, так как он содержит пароль:
rm -f init-app.sql
Сразу можно проверить, появились ли новые база и пользователь:
kubectl exec -n database deploy/mysql-single -- sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT user, host FROM mysql.user; SHOW DATABASES;"'
* в списке пользователей должен быть app, в списке БД — app.
Типовые проблемы
Под Pending / PVC
kubectl get pods -n database -l app.kubernetes.io/name=mysql-single -o name | xargs -I{} kubectl describe -n database {} | sed -n '/Events:/,$p'
kubectl get pvc -n database
kubectl get storageclass
* нет StorageClass или PVC Pending — поправьте storageClassName. Insufficient memory — уменьшите resources.memory и innodb_buffer_pool_size.
CrashLoop / права на том
kubectl logs -n database -l app.kubernetes.io/name=mysql-single --tail=100
* ошибки permission denied на /var/lib/mysql — проверьте fsGroup: 999 и mountPath /var/lib/mysql.
FailedMount / Multi-Attach
kubectl describe pod -n database -l app.kubernetes.io/name=mysql-single | sed -n '/Events:/,$p'
* при смене стратегии на RollingUpdate новый под не примонтирует RWO-том, пока жив старый — верните strategy.type: Recreate.
Конфиг не применяется
kubectl exec -n database deploy/mysql-single -- sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT @@innodb_buffer_pool_size, @@time_zone;"'
* после правки ConfigMap перезапустите под: kubectl delete pod -n database -l app.kubernetes.io/name=mysql-single (данные на PVC сохранятся). Если синтаксис в custom.cnf нарушен, MySQL не запустится и под уйдет в CrashLoopBackOff — перед перезапуском стоит перепроверить отступы и опечатки в параметрах.
Резервное копирование
PVC защищает от пересоздания пода, но не от повреждения данных, случайного DROP или потери самого тома. Регулярный бэкап через mysqldump стоит настроить отдельно — например, CronJob в том же namespace, который выгружает дамп во внешнее хранилище (S3, отдельный PV на другой ноде). Ручной дамп для разовой проверки:
kubectl exec -n database deploy/mysql-single -- sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" app' > app-backup.sql
* дамп сохраняется на хосте, с которого выполнена команда, а не в поде. Для продакшена дамп на локальный PVC того же кластера — недостаточная защита: при потере ноды или тома пропадут и данные, и бэкап.