The source code for this lab exercise is available on GitHub.
In our last article Investigating a failed VolumeSnapshot with NFS on Kubernetes, we saw how using NFS as a storage backend for stateful workloads on Kubernetes is not suitable in a production context due to fundamental limitations of NFS itself. So how are we to run our stateful applications on Kubernetes, if at all?
A common deployment model for running stateful applications on Kubernetes is the cloud native hybrid architecture where stateful components of an application such as a database or object storage run on virtual machines (VMs) for optimal stability and performance, while stateless components such as the frontend web UI or REST API server run on Kubernetes for maximal resiliency, elasticity and high availability. While this deployment model combines the best of both worlds, configuration is complex compared to deploying the entire application in-cluster since the stateless components running on Kubernetes must be explicitly configured to point to the out-of-cluster stateful components instead of leveraging the native service discovery mechanisms offered by Kubernetes.
Another option is to leverage a Kubernetes-native distributed storage solution such as Rook Ceph as the storage backend for stateful components running on Kubernetes. This has the benefit of simplifying application configuration while addressing business requirements for data backup and recovery such as the ability to take volume snapshots at a regular interval and perform application-level data recovery in case of a disaster.
Rook Ceph is a CNCF Graduated project. As its name might suggest, Rook Ceph is comprised of two major components: Ceph and Rook. Ceph is the distributed storage system itself while Rook is the Kubernetes operator for automated setup and lifecycle management of Ceph clusters which greatly simplifies the deployment and administration of Ceph clusters on Kubernetes.
In the lab to follow, we’ll quickly provision a 3-node kubeadm cluster (1 master, 2 workers) on the cloud provider of your choice using an automation stack comprised of OpenTofu and Ansible, then deploy Rook Ceph using the official Helm charts and confirm that we are now able to successfully create CSI volume snapshots from PVCs by reusing the MinIO example from our last article.
Lab: Deploying Rook Ceph to a 3-node kubeadm cluster
This lab has been tested with Kubernetes v1.29.0 (Mandala).
Prerequisites
Familiarity with Kubernetes cluster administration is assumed. Furthermore, it is assumed that you are already familiar with Kubernetes CSI and related API objects such as VolumeSnapshots and VolumeSnapshotClasses. If not, it is recommended to follow through the lab at Investigating a failed VolumeSnapshot with NFS on Kubernetes before attempting this lab.
Setting up your environment
A Unix/Linux environment is required. If on Windows, make sure WSL2 is installed and follow the lab within WSL2.
The automated 3-node kubeadm cluster setup is comprised of 2 components: OpenTofu and Ansible. OpenTofu is an open source drop-in replacement for the infamous Terraform infrastructure-as-code (IaC) tool governed by the Linux Foundation, while Ansible is an automated scripting and configuration management tool. We’ll use OpenTofu to provision the necessary infrastructure on the cloud provider of your choice (currently AWS and Alibaba Cloud are supported), then switch over to Ansible for running the scripts required to set up a bare-bones Kubernetes cluster on our cloud infrastructure.
In case you do not have an AWS or Alibaba Cloud account and do not wish to create one, it is still possible to follow through this lab by manually provisioning the required infrastructure on the cloud provider of your choice, on-premises as VMs or on bare metal:
- 3 nodes running Ubuntu 22.04 LTS
- At least 8 vCPUs per node
- At least 32 GiB memory per node
- At least 16 GiB for the system disk per node
- At least 1 unpartitioned, unformatted 64 GiB data disk per node
- SSH with public key authentication must be enabled for all nodes (or customize the Ansible playbook accordingly)
- All nodes must have an account with the same username and SSH key (or customize the Ansible playbook accordingly)
- Passwordless
sudomust be configured (or customize the Ansible playbook accordingly)
You are then free to skip the step involving OpenTofu and invoke ansible-playbook directly, after customizing the 2 files detailed below with configuration values matching your own infrastructure.
ansible/ansible.cfg
[defaults]inventory = ./hosts.yamlremote_user = ubuntuprivate_key_file = /path/to/your/key.pemhost_key_checking = False
ansible/hosts.yaml
masters: hosts: master0: ansible_host: x.x.x.x private_ip: x.x.x.xworkers: hosts: worker0: ansible_host: x.x.x.x worker1: ansible_host: x.x.x.x
The rest of the instructions will assume an AWS environment, though note that Alibaba Cloud is also supported with minimal modification, namely by setting the following environment variables:
export CLOUD_PROVIDER="aliyun"export TF_VAR_aliyun_access_key="XXXXXXXXXXXXXXXXXXXXXXXX" # replace me!export TF_VAR_aliyun_secret_key="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # replace me!
Setting up your AWS account
You’ll need to set up your AWS account and create an IAM administrator, then generate an access key and secret key for your IAM administrator for setting up AWS CLI v2 in our next step. Consult the official AWS documentation if in doubt.
Setting up AWS CLI v2
First make sure ~/.local/bin/ exists and is in your PATH so sudo is not required for installing the various command-line tools in this lab on your local laptop / desktop / workstation.
mkdir -p "$HOME/.local/bin/"echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"source "$HOME/.bashrc"
Now download AWS CLI v2 from the official website and install it using the provided script:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"unzip -n awscliv2.zip./aws/install --bin-dir "$HOME/.local/bin/" --install-dir "$HOME/.local/aws-cli/"
You’ll also need to configure AWS CLI v2 with your access and secret keys when prompted:
aws configure
Confirm that the setup is functional:
aws ec2 describe-instances
Sample output:
{ "Reservations": []}
Installing OpenTofu
Now install OpenTofu using the official release binaries. The latest version is 1.6.0-rc1 at the time of writing.
wget https://github.com/opentofu/opentofu/releases/download/v1.6.0-rc1/tofu_1.6.0-rc1_linux_amd64.zipunzip -n tofu_1.6.0-rc1_linux_amd64.zipmv tofu "$HOME/.local/bin/"
Confirm that OpenTofu is correctly installed:
tofu -version
Sample output:
OpenTofu v1.6.0-rc1on linux_amd64
Installing Ansible
The recommended installation method is via pipx which needs to be first installed.
python3 -m pip install --user pipxpython3 -m pipx ensurepath
Now install Ansible with pipx. The latest version is 2.16.2 at the time of writing.
python3 -m pipx install --include-deps ansible
Confirm that Ansible is correctly installed:
ansible-playbook --version
Sample output:
ansible-playbook [core 2.16.2] config file = None configured module search path = ['/home/donaldsebleung/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /home/donaldsebleung/.local/share/pipx/venvs/ansible/lib/python3.10/site-packages/ansible ansible collection location = /home/donaldsebleung/.ansible/collections:/usr/share/ansible/collections executable location = /home/donaldsebleung/.local/bin/ansible-playbook python version = 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] (/home/donaldsebleung/.local/share/pipx/venvs/ansible/bin/python) jinja version = 3.1.2 libyaml = True
Deploying the automation stack
Clone the project repository and make it your working directory.
git clone https://github.com/DonaldKellett/kubeadm-1m2w.gitcd kubeadm-1m2w/
Now invoke OpenTofu to provision the necessary Amazon EC2 instances. Note that Rook Ceph has nontrivial resource requirements so 8 vCPUs and 32 GiB of memory per node are required, which is satisfied by the t3.2xlarge instance type.
export CLOUD_PROVIDER="aws"export TF_VAR_instance_type="t3.2xlarge"tofu -chdir="opentofu/${CLOUD_PROVIDER}/" inittofu -chdir="opentofu/${CLOUD_PROVIDER}/" apply
Answer yes when prompted. Once the provisioning is complete, make note of the k8s-master0-public-ip – you’ll need this to SSH into the master node for running kubectl commands.
Now wait a few seconds for the instances to stabilize and run the provided Ansible playbook which should take no longer than 5 minutes to complete:
export ANSIBLE_CONFIG="${PWD}/ansible/ansible.cfg"ansible-playbook "${PWD}/ansible/playbook.yaml"
Checking that everything is set up correctly
Now SSH into the master node using the public IP you noted earlier and check that all 3 nodes are up and ready:
kubectl get nodes
Sample output:
NAME STATUS ROLES AGE VERSIONmaster0 Ready control-plane 3m43s v1.29.0worker0 Ready <none> 102s v1.29.0worker1 Ready <none> 102s v1.29.0
Let’s also take a look at the disks available to the master node (also available to each worker node):
sudo lsblk -f
Sample output:
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTSloop0 0 100% /snap/amazon-ssm-agent/7628loop1 0 100% /snap/core18/2812loop2 0 100% /snap/core20/2015loop3 0 100% /snap/lxd/24322loop4 0 100% /snap/snapd/20290nvme1n1 nvme0n1 ├─nvme0n1p1 ext4 1.0 cloudimg-rootfs 9e71e708-e903-4c26-8506-d85b84605ba0 11G 28% /├─nvme0n1p14 └─nvme0n1p15 vfat FAT32 UEFI A62D-E731 98.3M 6% /boot/efi
Notice that the 64 GiB data disk nvme1n1 is unformatted and unpartitioned. This is important as our Ceph cluster will use this data disk later (on each node) for providing storage to our stateful Kubernetes applications.
All subsequent commands in this lab should be executed on the Kubernetes master node unless otherwise instructed.
Installing Rook Ceph
Before installing Rook Ceph, we’ll need to install a few dependencies:
- The
VolumeSnapshot,VolumeSnapshotContentandVolumeSnapshotClassCRDs - Helm
- The CSI snapshot controller
Install the CRDs:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yamlkubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yamlkubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
Confirm the CRDs are correctly installed:
kubectl api-resources | grep volumesnapshot
Sample output:
volumesnapshotclasses vsclass,vsclasses snapshot.storage.k8s.io/v1 false VolumeSnapshotClassvolumesnapshotcontents vsc,vscs snapshot.storage.k8s.io/v1 false VolumeSnapshotContentvolumesnapshots vs snapshot.storage.k8s.io/v1 true VolumeSnapshot
Now ensure ~/.local/bin/ is in your PATH so sudo is not required for installing Helm:
mkdir -p "$HOME/.local/bin/"echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"source "$HOME/.bashrc"
Next, install Helm:
wget https://get.helm.sh/helm-v3.13.3-linux-amd64.tar.gztar xvf helm-v3.13.3-linux-amd64.tar.gzmv linux-amd64/helm "$HOME/.local/bin/"
Confirm Helm is correctly installed:
helm version
Sample output:
version.BuildInfo{Version:"v3.13.3", GitCommit:"c8b948945e52abba22ff885446a1486cb5fd3474", GitTreeState:"clean", GoVersion:"go1.20.11"}
Now we can install the snapshot controller via a Helm chart using the default values.
helm repo add democratic-csi https://democratic-csi.github.io/charts/helm repo updatehelm -n kube-system install \ snapshot-controller \ democratic-csi/snapshot-controller \ --version 0.2.4
Installing the Rook operator
The Rook operator performs lifecycle management functions of Ceph clusters and must be installed prior to the Ceph cluster itself. By default, it expects to be deployed in the namespace rook-ceph, though this behavior is configurable.
Remember to specify the option enableDiscoveryDaemon=true which automatically scans each node on a regular interval for clean (unformatted and unpartitioned), newly attached data disks to be added to the storage pool.
helm repo add rook-release https://charts.rook.io/releasehelm repo updatehelm -n rook-ceph install \ rook-ceph \ rook-release/rook-ceph \ --set enableDiscoveryDaemon=true \ --create-namespace
Now wait for the operator and per-node discovery daemon to enter a Ready state:
kubectl -n rook-ceph wait \ --for=condition=Ready \ pods \ -l 'app in (rook-ceph-operator, rook-discover)' \ --timeout=180s
Sample output:
pod/rook-ceph-operator-775858d6b-98qsk condition metpod/rook-discover-ln4gp condition metpod/rook-discover-tgzdn condition metpod/rook-discover-v779r condition met
If you only see the first line of output, re-run the command above since the discovery daemon pods are not created until the operator is up and running.
Installing the Ceph cluster
Now install the Ceph cluster – we’ll briefly go through each of the key components in a moment.
Ensure that the following Helm chart values are set:
cephBlockPoolsVolumeSnapshotClass.enabled=truefor generating the RBDVolumeSnapshotClassautomaticallycephFileSystemVolumeSnapshotClass.enabled=truefor generating the Ceph filesystemVolumeSnapshotClassautomatically
helm -n rook-ceph install \ rook-ceph-cluster \ rook-release/rook-ceph-cluster \ --set cephBlockPoolsVolumeSnapshotClass.enabled=true \ --set cephFileSystemVolumeSnapshotClass.enabled=true
Now wait for the Ceph cluster to enter a Ready state – this may take up to 15 minutes:
kubectl -n rook-ceph wait --for=condition=Ready cephclusters --all --timeout=900s
Sample output:
cephcluster.ceph.rook.io/rook-ceph condition met
Let’s take a closer look at our Ceph cluster.
kubectl -n rook-ceph get cephcluster rook-ceph
Sample output:
NAME DATADIRHOSTPATH MONCOUNT AGE PHASE MESSAGE HEALTH EXTERNAL FSIDrook-ceph /var/lib/rook 3 45m Ready Cluster created successfully HEALTH_OK 5d6cc90c-87f1-4de7-8b6d-8c18562a9fa0
HEALTH_OK indicates that our Ceph cluster is healthy. To really see what’s going on, it may help to view the created pods.
kubectl -n rook-ceph get pods -l 'app,app notin (rook-ceph-operator, rook-discover)'
Sample output:
NAME READY STATUS RESTARTS AGEcsi-cephfsplugin-4jrkb 2/2 Running 1 (46m ago) 47mcsi-cephfsplugin-provisioner-cd88d8c4-fcm9g 5/5 Running 1 (46m ago) 47mcsi-cephfsplugin-provisioner-cd88d8c4-jbwcb 5/5 Running 0 47mcsi-cephfsplugin-pzbgx 2/2 Running 0 47mcsi-cephfsplugin-w6f6h 2/2 Running 1 (46m ago) 47mcsi-rbdplugin-gbrc8 2/2 Running 1 (46m ago) 47mcsi-rbdplugin-kl6cd 2/2 Running 1 (46m ago) 47mcsi-rbdplugin-provisioner-f6d4c9775-wlb9n 5/5 Running 1 (46m ago) 47mcsi-rbdplugin-provisioner-f6d4c9775-xvvwq 5/5 Running 0 47mcsi-rbdplugin-s89gt 2/2 Running 0 47mrook-ceph-crashcollector-master0-5787b75899-xh62s 1/1 Running 0 41mrook-ceph-crashcollector-worker0-6d859cbd7-tpcvm 1/1 Running 0 42mrook-ceph-crashcollector-worker1-6f8d676454-hrlfx 1/1 Running 0 37mrook-ceph-mds-ceph-filesystem-a-5977dbcfc8-m6tp7 2/2 Running 0 42mrook-ceph-mds-ceph-filesystem-b-57d57cc6d4-v542c 2/2 Running 0 41mrook-ceph-mgr-a-6d48d5cd4-l5jq5 3/3 Running 0 45mrook-ceph-mgr-b-7b944ddd97-skknv 3/3 Running 0 45mrook-ceph-mon-a-764cbbbd9f-hdmtx 2/2 Running 0 47mrook-ceph-mon-b-5b6cd88d59-mh4p4 2/2 Running 0 45mrook-ceph-mon-c-795c785489-p65nr 2/2 Running 0 45mrook-ceph-osd-0-bb954988-86rx6 2/2 Running 0 44mrook-ceph-osd-1-766d6f9dd5-ct4np 2/2 Running 0 44mrook-ceph-osd-2-6f9df74867-ntjjg 2/2 Running 0 44mrook-ceph-osd-prepare-master0-4gpbg 0/1 Completed 0 43mrook-ceph-osd-prepare-worker0-zkvdm 0/1 Completed 0 43mrook-ceph-osd-prepare-worker1-nlq89 0/1 Completed 0 43mrook-ceph-rgw-ceph-objectstore-a-86cc9554df-476vf 2/2 Running 0 37m
That’s a lot of pods! We’ll focus on the important ones and skip the rest:
- Ceph monitor (MON): responsible for maintaining the state of the cluster. Like etcd, it relies on a quorum so 3 MONs are required for high availability. On Kubernetes, they run as Pods
rook-ceph-mon-{a,b,c}*, one on each node - Ceph manager (MGR): responsible for interacting with external monitoring and management systems. Runs in active-standby mode so 2 replicas are sufficient. On Kubernetes, they run as Pods
rook-ceph-mgr-{a,b}* - Ceph OSD: daemon for interacting with logical volumes, one per volume. Stands for “Object Storage Daemon”. On Kubernetes, they run as Pods
rook-ceph-osd-n*wherenis a non-negative integer - Ceph block device (RBD): also known as RADOS block device (hence the abbreviation RBD). Provides block-level storage to stateful Kubernetes applications
Now that our Ceph cluster is running, let’s reuse the MinIO example from our last article and confirm that we are now able to successfully create a VolumeSnapshot from a PVC. But before that, let’s confirm that we have a working StorageClass for creating PVCs and a working VolumeSnapshotClass for creating volume snapshots from our PVCs.
kubectl get storageclasskubectl get volumesnapshotclass
Sample output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGEceph-block (default) rook-ceph.rbd.csi.ceph.com Delete Immediate true 66mceph-bucket rook-ceph.ceph.rook.io/bucket Delete Immediate false 66mceph-filesystem rook-ceph.cephfs.csi.ceph.com Delete Immediate true 66mNAME DRIVER DELETIONPOLICY AGEceph-block rook-ceph.rbd.csi.ceph.com Delete 66mceph-filesystem rook-ceph.cephfs.csi.ceph.com Delete 66m
Observe that RBD is marked as the default StorageClass and has a corresponding VolumeSnapshotClass so we expect to be able to create snapshots from our PVCs normally.
Installing MinIO and creating a VolumeSnapshot
This is much the same as with our previous lab.
Add the Bitnami repo and install MinIO:
helm repo add bitnami https://charts.bitnami.com/bitnamihelm repo updatehelm -n minio install \ minio \ bitnami/minio \ --version 12.9.4 \ --create-namespace
Now wait for it to become ready, which should take no longer than 5 minutes:
kubectl -n minio wait --for=condition=Ready pods --all --timeout=300s
Observe that a PVC minio is created and bound:
kubectl -n minio get pvc minio
Now create a snapshot from our MinIO PVC:
kubectl -n minio apply -f - << EOF---apiVersion: snapshot.storage.k8s.io/v1kind: VolumeSnapshotmetadata: name: miniospec: volumeSnapshotClassName: ceph-block source: persistentVolumeClaimName: minioEOF
Wait for our snapshot to become ready:
kubectl -n minio wait \ --for=jsonpath='{.status.readyToUse}'=true \ volumesnapshot \ minio \ --timeout=180s
Sample output:
volumesnapshot.snapshot.storage.k8s.io/minio condition met
Cleaning up
Unless you wish to continue experimenting with your cluster, log out of the master node now, ensure that your working directory is set to the kubeadm-1m2w/ project you cloned earlier and run the following commands on your own laptop / desktop / workstation to clean up the lab resources to save costs:
export CLOUD_PROVIDER="aws"tofu -chdir="opentofu/${CLOUD_PROVIDER}/" destroy
Answer yes when prompted.
Concluding remarks and going further
By deploying the production-grade Rook Ceph CSI distributed storage backend to Kubernetes, it is possible to run stateful applications directly on Kubernetes in a production context and combine it with integrated backup and disaster recovery (DR) solutions capable of cross-cluster application-level data recovery such as Velero and Kasten K10 to meet your business continuity, legal, audit and compliance requirements, all without sacrificing the ability to leverage Kubernetes’ powerful features and abstractions such as built-in service discovery mechanisms for managing your entire application. However, in rare cases where performance, stability and permanence* are of utmost importance, it may still make sense to deploy specific stateful component(s) outside of Kubernetes and adopt a partial cloud native hybrid model, just for those affected components.
Storage (CSI) on Kubernetes has always been a tricky subject and has taken a long time to mature compared to other fundamental capabilities such as compute (CRI) and networking (CNI), leaving IT practitioners and managers the impression that “Kubernetes is best for running stateless workloads, run stateful workloads in VMs”. However, as Kubernetes’ storage capabilities mature over time, this fundamental notion is under challenge. So, what do you think?
* From the GitLab 3k reference architecture with a cloud native hybrid deployment model:
Hybrid installations leverage the benefits of both cloud native and traditional compute deployments. With this, stateless components can benefit from cloud native workload management benefits while stateful components are deployed in compute VMs with Linux package installations to benefit from increased permanence.
Leave a comment