Run a StatefulSet MySQL on k8s cluster

Posted on Aug 2, 2024

configmap.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
data:
  primary.cnf: |
    # Apply this config only on the primary.
    [mysqld]
    log-bin
    max_allowed_packet=128M
    skip-name-resolve    
  replica.cnf: |
    # Apply this config only on replicas.
    [mysqld]
    super-read-only
    max_allowed_packet=128M
    skip-name-resolve    
k create -f configmap.yml

configmap-conf-db.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-configmap
#  namespace: ...
data:
  init.sql: |
    CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';
    GRANT ALL PRIVILEGES ON '$MYSQL_DATABASE'.* TO '$MYSQL_USER'@'%';
    FLUSH PRIVILEGES;

    USE '$MYSQL_DATABASE';    
k create -f configmap-conf-db.yml

Encode credentials.

echo -n testuser | base64 
dGVzdHVzZXI=

echo -n testuserpass | base64 
dGVzdHVzZXJwYXNz

echo -n testdb | base64 
dGVzdGRi

echo -n rootpass | base64 
cm9vdHBhc3M=

Use sops and age or gpg to encrypt sensetive data for more secure.

secrets.yml

apiVersion: v1
kind: Secret
metadata:
  name: mysql-cred
#  namespace: ...
type: Opaque
data:
  username: dGVzdHVzZXI=
  password: dGVzdHVzZXJwYXNz
  dbname: dGVzdGRi
  rootpass: cm9vdHBhc3M=
k create -f secrets.yml

services.yml

# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mysql
---
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the primary: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
    readonly: "true"
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql
k create -f services.yml

statefulset.yml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
      app.kubernetes.io/name: mysql
  serviceName: mysql
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql
        app.kubernetes.io/name: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:9.0.1
        command:
        - bash
        - "-c"
        - |
          set -ex
          # Generate mysql server-id from pod ordinal index.
          [[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          # Add an offset to avoid reserved server-id=0 value.
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          # Copy appropriate conf.d files from config-map to emptyDir.
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/primary.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/replica.cnf /mnt/conf.d/
          fi                    
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      containers:
      - name: mysql
        image: mysql:9.0.1
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-cred
              key: rootpass
        - name: MYSQL_USER
          valueFrom:
            secretKeyRef:
              name: mysql-cred
              key: username
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-cred
              key: password
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: mysql-cred
              key: dbname
        ports:
        - name: mysql
          containerPort: 3306
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 512Mi
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "localhost"] # todo: mysql -h127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD -e'SELECT 1' --> Using a password on the command line interface can be insecure.
          initialDelaySeconds: 30
          periodSeconds: 2
          timeoutSeconds: 1
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "localhost"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        - name: mysql-config
          mountPath: /docker-entrypoint-initdb.d
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql
      - name: mysql-config
        configMap:
          name: mysql-configmap
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 256Mi
k create -f statefulset.yml

Checks

mysql -u root -p
mysql -u testuser -p

> show databases;

References