Using the Oracle MySQL Kubernetes database Operator, Portworx and OKE

Today there are a number of different MySQL Kubernetes Operators available for use, many providing similar functionality with varying development effort and support offerings, for example Percona, PressLabs, GrdsCloud, Moco to name a few.

In this post I am going to be using the official MySQL Kubernetes Operator developed by the Oracle MySQL team to manage the setup of a MySQL InnoDB Cluster on OKE with Portworx Storage.

Kubernets Environment

Before applying the MySQL Custom Resource Definition (CRD) and Operator, lets have a quick look at my Oracle Cloud Infrastructure (OCI) Oracle Container Engine for Kubernetes (OKE) environment.

The Kubernetes version with kubectl version

% kubectl version --short | awk -Fv '/Server Version: / {print $3}'

Number of nodes, operating system, and container runtime in the Kubernetes Cluster with kubectl get nodes.

% kubectl get nodes -o wide
NAME        STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE                KERNEL-VERSION                 CONTAINER-RUNTIME Ready  node  41h v1.20.8 <none>     Oracle Linux Server 7.6 4.14.35-1902.2.0.el7uek.x86_64 cri-o://1.20.2 Ready  node  41h v1.20.8 <none>     Oracle Linux Server 7.6 4.14.35-1902.2.0.el7uek.x86_64 cri-o://1.20.2  Ready  node  41h v1.20.8  <none>     Oracle Linux Server 7.6 4.14.35-1902.2.0.el7uek.x86_64 cri-o://1.20.2

And Portworx version using pxctl –version

% pxctl --version
pxctl version

If you are not already using Portworx and want to install the Free Forever Portworx version into your OKE environment you may want to read blog post: Portworx Essentials installation on Oracle Kubernetes Engine (OKE)

Storage Class

Let’s start by creating a Portworx Kubernetes Container Storage Interface (CSI) storage class.

% cat px-mysql-csi-sc.yaml 
kind: StorageClass
  name: px-mysql-csi-sc
  repl: "1"
  io_profile: "auto"

And apply with kubectl apply

% kubectl apply -f px-mysql-csi-sc.yaml created

We can examine the settings with kubectl describe, for example, from the below you can see I have a replication factor of 1 and io_profile of auto.

% kubectl describe sc/px-mysql-csi-sc
Name:            px-mysql-csi-sc
IsDefaultClass:  No

Parameters:            io_profile=auto,repl=1
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     Immediate
Events:                <none>

You can list the StorageClasses in your cluster with kubectl get sc, the default StorageClass is marked with (default) for example:

% kubectl get sc 
oci (default)                          Delete        Immediate            false          20h
oci-bv                Delete        WaitForFirstConsumer false          20h
px-csi-sc                            Delete        Immediate            false          20h
px-db                   Delete        Immediate            true           20h
px-db-cloud-snapshot    Delete        Immediate            true           20h
px-db-cloud-snapshot-encrypted   Delete        Immediate            true           20h
px-db-encrypted         Delete        Immediate            true           20h
px-db-local-snapshot    Delete        Immediate            true           20h
px-db-local-snapshot-encrypted   Delete        Immediate            true           20h
px-mysql-csi-sc                      Delete        Immediate            false          17m
px-replicated           Delete        Immediate            true           20h
px-replicated-encrypted   Delete        Immediate            true           20h
stork-snapshot-sc              stork-snapshot                  Delete        Immediate            true           20h

Out-of-the box the MySQL Operator will use the default Kubernetes storage class, however if an alternative StorageClass is prefered we can easily change the default by using kubectl patch to set is-default-class.

First set is-default-class for the current default Storage Class to false, for example.

% kubectl patch storageclass oci -p '{"metadata": {"annotations":{"":"false"}}}' patched

And then update the preferred Storage Class is-default-class to true.

% kubectl patch storageclass px-mysql-csi-sc -p '{"metadata": {"annotations":{"":"true"}}}' patched

We can see the change with kubectl get storageclass

% kubectl get storageclass                 
oci                                    Delete        Immediate            false          20h
px-mysql-csi-sc (default)                Delete        Immediate            false          28m


OK now let’s install the Oracle MySQL Custom Resource Definition (CRD) and Operator.

For this blog I will install directly into my OKE Kubernetes Cluster using kubectl apply.

% kubectl apply -f created created created created

And again using kubectl apply to deploy operator.

% kubectl apply -f

serviceaccount/mysql-sidecar-sa created created created created created
namespace/mysql-operator created
serviceaccount/mysql-operator-sa created
deployment.apps/mysql-operator created

From the above we can see a number of service accounts and new namespace called mysql-operator has been created.

GitHub method

Alternatively, the Oracle MySQL CRD and Operator can also be downloaded from GitHub

# git clone
Cloning into 'mysql-operator'...
remote: Enumerating objects: 1065, done.
remote: Counting objects: 100% (1065/1065), done.
remote: Compressing objects: 100% (370/370), done.
remote: Total 1065 (delta 682), reused 1064 (delta 681), pack-reused 0
Receiving objects: 100% (1065/1065), 4.60 MiB | 677.00 KiB/s, done.
Resolving deltas: 100% (682/682), done.
# cd /root/mysql-operator/deploy
# ls -l
total 20
-rw-r--r-- 1 root root 8888 Jul 20 09:25 deploy-crds.yaml
-rw-r--r-- 1 root root 2905 Jul 20 09:25 deploy-operator.yaml
-rwxr-xr-x 1 root root 3347 Jul 20 09:25

And then install using the kubectl apply as before.

Custom Resource Definitions

If we use kubectl get crd to list all Custom Resource Definitions, we can now see the four new CRDs

% kubectl get crd
NAME                                                         CREATED AT                 2021-08-16T16:24:48Z                  2021-08-16T16:25:44Z          2021-08-16T16:26:00Z                   2021-08-16T16:25:55Z            2021-08-16T16:25:39Z                 2021-08-16T16:25:50Z            2021-08-16T16:24:43Z                  2021-08-16T16:24:43Z                     2021-08-16T16:25:34Z              2021-08-16T16:25:24Z                2021-08-16T16:25:29Z                              2021-08-17T12:32:41Z                        2021-08-16T16:25:14Z                2021-08-16T16:25:09Z                              2021-08-17T12:32:39Z                                     2021-08-17T12:32:41Z                          2021-08-16T16:25:19Z                  2021-08-16T16:25:19Z                                2021-08-17T12:32:40Z          2021-08-16T16:24:53Z                               2021-08-16T16:24:43Z                    2021-08-16T16:24:48Z                      2021-08-16T16:23:10Z                         2021-08-16T16:23:16Z                        2021-08-16T16:24:01Z   2021-08-16T16:24:58Z              2021-08-16T16:25:04Z       2021-08-16T16:24:58Z             2021-08-16T16:24:59Z

Review details

Use kubectl get deployment / deploy to confirm we have an mysql-operator deployment.

% kubectl get deployment -n mysql-operator
mysql-operator   1/1     1            1           27m

And kubectl get pod to get the the MySQL operator pod name

% kubectl get pods -n mysql-operator
NAME                              READY   STATUS    RESTARTS   AGE
mysql-operator-6fd4df855f-zqmgh   1/1     Running   0          28m

Using kubectl describe we can see the deployment is using a MySQL 8.0.25 image from the Docker Hub repository.

% kubectl describe deployment/mysql-operator -n mysql-operator
Name:                   mysql-operator
Namespace:              mysql-operator
CreationTimestamp:      Tue, 17 Aug 2021 13:33:16 +0100
Labels:                 version=1.0
Annotations:   1
Selector:               name=mysql-operator
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:           name=mysql-operator
  Service Account:  mysql-operator-sa
    Image:      mysql/mysql-operator:8.0.25-2.0.1
    Port:       <none>
    Host Port:  <none>
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   mysql-operator-6fd4df855f (1/1 replicas created)
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  29m   deployment-controller  Scaled up replica set mysql-operator-6fd4df855f to 1

Creating InnoDB Cluster

Now we have a MySQL Operator started we can create an InnoDB Cluster.

Create Secret

Create a Kubernetes secret to store the MySQL root password using the kubectl create secret command or the example yaml file below to create a secret called mypwds

# cat mysql-secret.yaml 
apiVersion: v1
kind: Secret
  name: mypwds
  rootUser: root
  rootHost: '%'
  rootPassword: MySQL202!

% kubectl apply -f mysql-secret.yaml 
secret/mypwds created

Setup MySQL InnoDB Cluster

The MySQL operator will create:

  • A statefulset and service for the MySQL server called mycluster.
    • pods mycluster-0..2
    • sidecar container agents
  • A replicaSet for the MySQL router called mycluster-router
    • pods mycluster-router-0, mysqlcluster-router-1
  • A service for the MySQL InnoDB Cluster called mycluster

The example below will create a 3 node MySQL InnoDB Cluster with 2 MySQL routers using the secret previously created.

% cat mysql-cluster.yaml 
kind: InnoDBCluster
  name: mycluster
  secretName: mypwds
  instances: 3
    instances: 2
Oracle MySQL database Operator  Architecture
MySQL Architecture
% kubectl apply -f mysql-cluster.yaml created

Use –watch or -w flag to start watching updates, from here we can see the 3 instances and 2 routers previously defined.

% kubectl get innodbcluster --watch
mycluster   ONLINE   3        3           2         9m30s

Persistent Volume Claims

We can see the MySQL Operator created multiple 2GB ‘DATA’ Persistent Volume Claim (PVC) using the px-mysql-csi-sc StorageClass.

 % kubectl get pvc
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
datadir-mycluster-0   Bound    pvc-25ffa7e2-fb1f-4261-a26d-2fec8e2e5d89   2Gi        RWO            px-mysql-csi-sc   47m
datadir-mycluster-1   Bound    pvc-d702b083-86c6-4355-afa1-67f8a7876dad   2Gi        RWO            px-mysql-csi-sc   40m
datadir-mycluster-2   Bound    pvc-f54d399d-afca-4ff7-9bc5-0b1317d57ed4   2Gi        RWO            px-mysql-csi-sc   18m

We can also see the newly provisioned volume from Portworx using pxctl volume list.

% pxctl volume list                
876981642026201908	pvc-25ffa7e2-fb1f-4261-a26d-2fec8e2e5d89	2 GiB	1	no	no		no		LOW		up - attached on	no
372385243185435194	pvc-d702b083-86c6-4355-afa1-67f8a7876dad	2 GiB	1	no	no		no		LOW		up - attached on	no
120683905232179905	pvc-f54d399d-afca-4ff7-9bc5-0b1317d57ed4	2 GiB	1	no	no		no		LOW		up - attached on	no

MySQL InnoDB Cluster

We can use kubectl get service to determine ports being used.

% kubectl get service mycluster
NAME        TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                               AGE
mycluster   ClusterIP   <none>        6446/TCP,6448/TCP,6447/TCP,6449/TCP   8m23s
% kubectl describe service mycluster
Name:              mycluster
Namespace:         kube-system
Annotations:       <none>
Selector:          component=mysqlrouter,,tier=mysql
Type:              ClusterIP
IP Families:       <none>
Port:              mysql  6446/TCP
TargetPort:        6446/TCP
Port:              mysqlx  6448/TCP
TargetPort:        6448/TCP
Port:              mysql-ro  6447/TCP
TargetPort:        6447/TCP
Port:              mysqlx-ro  6449/TCP
TargetPort:        6449/TCP
Session Affinity:  None
Events:            <none>


In this post I have shared how we can use Oracle MySQL Kubernetes Operator to create an MySQL InnoDB cluster on Kubernetes with Portworx storage.

In my next MySQL on Kubernetes post I will share how we can connect to the database and perform a benchmark.

[twitter-follow screen_name=’RonEkins’ show_count=’yes’]

Leave a Reply

Create a website or blog at

Up ↑

%d bloggers like this: