All Products
Search
Document Center

Container Service for Kubernetes:Deploy a stateful and high-availability MySQL cluster on ACK Serverless

Last Updated:Mar 26, 2026

Deploy a stateful, high-availability MySQL cluster on ACK Serverless

ACK Serverless lets you use native Kubernetes scheduling semantics to spread pods across multiple availability zones without managing nodes. This guide walks you through deploying a three-replica MySQL StatefulSet on ACK Serverless so that each replica runs in a different zone, giving you zone-level fault tolerance for stateful workloads.

Important The resource limits and MySQL configuration in this guide are sized for demonstration purposes. Review and adjust CPU, memory, and MySQL flags before deploying to production.

By the end of this guide, you will have:

  • A mysql namespace with a Secret and StorageClass

  • A MySQL StatefulSet (dbc1) with three replicas spread across three zones

  • Verified that each pod is scheduled to a different virtual node in a different zone

Prerequisites

Before you begin, ensure that you have:

How it works

The StatefulSet uses two complementary scheduling constraints to achieve zone-level high availability:

  • Topology spread constraints (topologySpreadConstraints): distributes pods evenly across zones. With maxSkew: 1 and topologyKey: topology.kubernetes.io/zone, the scheduler ensures no zone hosts more than one extra replica compared to any other zone.

  • Pod anti-affinity (podAntiAffinity): enforces node-level separation. Even within the same zone, each pod must run on a different virtual node. This prevents two replicas from sharing a single node.

Both constraints are required because topology spread alone controls zone distribution but does not prevent two pods from landing on the same node within a zone. Pod anti-affinity fills that gap by enforcing strict node-level separation.

Storage is provisioned by the fast-storageclass StorageClass using WaitForFirstConsumer binding. Each PersistentVolumeClaim (PVC) is not created until a pod is scheduled, which ensures the cloud disk is provisioned in the same zone as the pod — a requirement for zone-aware storage.

Important reclaimPolicy: Retain means that PVCs are not deleted automatically when you scale down the StatefulSet. To release storage, delete the PVCs manually after scaling down.

Step 1: Clone the example repository

  1. Clone the repository.

    git clone https://github.com/AliyunContainerService/serverless-k8s-examples.git
  2. Navigate to the working directory and list the YAML files.

    cd serverless-k8s-examples/stateful-mysql
    ls -al

    Expected output:

    README.md		secret.yaml		stateful-mysql.yaml	 storageclass.yaml

    The three YAML files you will apply:

    File Creates
    secret.yaml Secret for MySQL passwords
    storageclass.yaml StorageClass for per-zone cloud disk provisioning
    stateful-mysql.yaml MySQL StatefulSet with zone-spread scheduling

Step 2: Create the namespace, Secret, and StorageClass

  1. Create the mysql namespace.

    kubectl create namespace mysql

    Expected output:

    namespace/mysql created
  2. Create the Secret that stores MySQL passwords.

    kubectl apply -n mysql -f secret.yaml

    Expected output:

    secret/mysql-secret created

    The following is the YAML file for the Secret.

    Click to view the YAML file

    apiVersion: v1
    kind: Secret
    metadata:
      name: mysql-secret
    type: Opaque
    data:
      password: VGVzdFBhc3N3b3JkMTIz            # Base64-encoded MySQL root password
      admin-password: QWRtaW5QYXNzd29yZDQ1Ng==  # Base64-encoded MySQL admin password

    To generate Base64-encoded values for your own passwords:

    # Encode the root password
    echo -n "your-password" | base64
    
    # Encode the admin password
    echo -n "your-admin-password" | base64
  3. Create the StorageClass.

    kubectl apply -f storageclass.yaml

    Expected output:

    storageclass.storage.k8s.io/fast-storageclass created

    The following is the YAML file for the StorageClass.

    Click to view the YAML file

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: fast-storageclass
    provisioner: diskplugin.csi.alibabacloud.com
    parameters:
      type: cloud_auto,cloud_essd,cloud_ssd # Selects disk type by priority; actual type depends on node instance type and zone availability
      fstype: ext4
      diskTags: "a:b,b:c"
      encrypted: "false"
      performanceLevel: PL1
      volumeExpandAutoSnapshot: "forced" # Takes effect only when the disk type is cloud_essd
      provisionedIops: "40000"
      burstingEnabled: "false"
    volumeBindingMode: WaitForFirstConsumer # Delays disk creation until a pod is scheduled, ensuring the disk and pod are in the same zone
    reclaimPolicy: Retain
    allowVolumeExpansion: true

Step 3: Deploy the MySQL StatefulSet

  1. Apply the StatefulSet manifest. This creates three MySQL replicas spread across zones using topology spread constraints and pod anti-affinity.

    kubectl apply -n mysql -f stateful-mysql.yaml

    Expected output:

    statefulset.apps/dbc1 created

    The following is the YAML file for the StatefulSet.

    Click to view the YAML file

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: dbc1
      labels:
        app: mysql
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: mysql
      serviceName: mysql
      template:
        metadata:
          labels:
            app: mysql
        spec:
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: "topology.kubernetes.io/zone"
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: mysql
          affinity:
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchExpressions:
                      - key: app
                        operator: In
                        values:
                          - mysql
                  topologyKey: "kubernetes.io/hostname"
          containers:
            - name: mysql
              image: registry.cn-hangzhou.aliyuncs.com/kubeway/hub:mysql-server_8.0.28
              command:
                - /bin/bash
              args:
                - -c
                - >-
                  /entrypoint.sh
                  --server-id=$((20 +  $(echo $HOSTNAME | grep -o '[^-]*$') + 1))
                  --report-host=${HOSTNAME}.mysql.mysql.svc.cluster.local
                  --binlog-checksum=NONE
                  --enforce-gtid-consistency=ON
                  --gtid-mode=ON
                  --default-authentication-plugin=mysql_native_password
              env:
                - name: MYSQL_ROOT_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: mysql-secret
                      key: password
                - name: MYSQL_ADMIN_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: mysql-secret
                      key: admin-password
                - name: MYSQL_ROOT_HOST
                  value: "%"
              ports:
                - name: mysql
                  containerPort: 3306
                - name: mysqlx
                  containerPort: 33060
                - name: xcom
                  containerPort: 33061
              resources:
                limits:
                  cpu: "500m"
                  ephemeral-storage: "1Gi"
                  memory: "1Gi"
                requests:
                  cpu: "500m"
                  ephemeral-storage: "1Gi"
                  memory: "1Gi"
              volumeMounts:
                - name: mysql
                  mountPath: /var/lib/mysql
                  subPath: mysql
              readinessProbe:
                exec:
                  command:
                    - bash
                    - "-c"
                    - |
                      mysql -h127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD -e'SELECT 1'
                initialDelaySeconds: 5
                periodSeconds: 2
                timeoutSeconds: 1
              livenessProbe:
                exec:
                  command:
                    - bash
                    - "-c"
                    - |
                      mysqladmin -uroot -p$MYSQL_ROOT_PASSWORD ping
                initialDelaySeconds: 30
                periodSeconds: 10
                timeoutSeconds: 5
      updateStrategy:
        rollingUpdate:
          partition: 0
        type: RollingUpdate
      volumeClaimTemplates:
        - metadata:
            name: mysql
            labels:
              app: mysql
          spec:
            storageClassName: fast-storageclass
            volumeMode: Filesystem
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 25Gi

Step 4: Verify zone distribution

  1. Confirm the StatefulSet is ready.

    kubectl get statefulset -n mysql

    Expected output:

    NAME   READY   AGE
    dbc1   3/3     7m21s
  2. Check pod placement across zones.

    kubectl -n mysql get po -o wide

    Expected output:

    NAME     READY   STATUS    RESTARTS   AGE     IP             NODE                            NOMINATED NODE   READINESS GATES
    dbc1-0   1/1     Running   0          2m28s   10.1.214.213   virtual-kubelet-cn-hangzhou-j   <none>           <none>
    dbc1-1   1/1     Running   0          106s    10.3.146.113   virtual-kubelet-cn-hangzhou-h   <none>           <none>
    dbc1-2   1/1     Running   0          64s     10.4.232.78    virtual-kubelet-cn-hangzhou-g   <none>           <none>

    The NODE column contains the zone identifier. In the example above, the three pods are scheduled to cn-hangzhou-j, cn-hangzhou-h, and cn-hangzhou-g, confirming that the cluster is spread across three separate zones.

What's next