MySQL Deployment в Kubernetes на одном хосте

Обновлено и опубликовано Опубликовано:

Используемые термины: MySQLKubernetes.

В инструкции разберем, как развернуть MySQL в Kubernetes без StatefulSet и без оператора — через обычный Deployment с одной репликой и отдельным PVC для данных. Пройдем весь путь: подготовим namespace, зададим пароль root через Secret, настроим параметры сервера через ConfigMap, поднимем том для данных и опубликуем сервис.

Создание 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 того же кластера — недостаточная защита: при потере ноды или тома пропадут и данные, и бэкап.

# DevOps # Kubernetes # MySQL # Базы данных # Контейнеризация # Серверы
Дмитрий Моск — частный мастер
Был ли вам полезен этот скрипт?

Да            Нет

Дмитрий Моск
— IT-специалист.
Настройка серверов, услуги DevOps.

Заказать настройку сервера баз данных

Нужна бесплатная консультация?

Скрипты

Hermes Agent в Docker Compose: gateway и dashboard

MySQL Deployment в Kubernetes на одном хосте

Пример docker-compose для развертывания тестовой SMS-лаборатории

PostgreSQL StatefulSet в Kubernetes на одном хосте

GitLab CE в Docker Compose за Traefik с HTTPS

Пример манифеста для запуска Swagger в Kubernetes

Пример скрипта для миграции запущенной виртуальной машины Proxmox с ZFS репликацией

Другие скрипты

Все статьи

Нужен скрипт? Опишите его назначение:





Реклама