Introduction:
Update: we have provided a practical application of the knowledge conveyed in this article with a new document on How to Deploy Pihole in Kubernetes here.
Now continuing with the contents of this blog…
One of the first questions for container storage provisioning is ‘why NFS? ‘ The answer to such inquiries is almost always ‘it depends.’ Here’s an overview of common storage protocols to determine the appropriate type for a Kubernetes system:
SMB/CIFS
– Abbreviation: SMB is Server Messaging Protocol/Common Internet File System
– Ports: 137/UDP 138/TCP 139/TCP 445/TCP
– Very chatty protocol – ideal for Windows environment
– More secured than NFS. Provides some security features
– Less scalable, ‘normal’ speed, and complex to setup (in Linux environments)
NFS
– Abbreviation: Network File System
– Ports: 111/TCP 111/UDP 2049/TCP 2049/UDP 1110/TCP 1110/UDP 4045/TCP 4045/UDP
– Less chatty – ideal for Linux environments
– Not a secured protocol – IP filtering is required as an access barrier as there are no username/password authentications
– Very scalable, fast, easy to setup
iSCSI
– Abbreviation: Internet Small Computer System Interface
– Ports: 860/TCP 860/UDP 3260/TCP 3260/UDP
– Less chatty – ideal for dedicated subnets for storage communcations
– Secured protocol – IP filtering as well as CHAP authentication (username/password)
– Less scalable, fast, more complex to setup (with networking knowledge necessary)
Step 0: Prepare Network File System (NFS) Share
There are many vendors of network storage appliances available on the market, many of which would support iSCSI, CIFS/SMB, NFS, FTP, SFTP, and even Rsync. Instructions on how to create a new NFS share would vary on each of those appliances.
In this example, we’re using OpenMediaVault (OVM), a derivative of FreeNAS with a difference in base OS of Debian Linux vs FreeBSD. Here’s a screenshot of a NFS share from within the OVM interface.
The important note here is that such a share would be set at the OS layer with Access Control List (ACL) of 750
(with nfs daemon as the owner) or 777
(world access) to enable read/write access. Moreover, NFS permissions would be RW,subtree_check,insecure
with client access allowed from the subnet where the external IP of the Kubernetes cluster would ingress/egress. That would be the same network as the worker nodes (e.g. 192.168.80.0/24, an arbitrary private subnet to be configured in this lab for K8)
Step 1: Test the NFS Share Access On Each Node
# Install prerequisite on the Master node
sudo apt-get install nfs-common
# Set variables
nfsShare=test # assuming that the 'test' share has already been created on the server
nfsServer=NasServerNameOrIP
mountPoint=/mnt/test
sudo mkdir $mountPoint
sudo mount -t nfs $nfsServer:/$nfsShare $mountPoint
# Example of success
# Nothing, no feedback output from CLI
# Example of failure which will require fixing the file share on the NFS server
brucelee@controller:$ sudo mount -t nfs $nfsServer:/$nfsShare $mountPoint
mount.nfs: access denied by server while mounting FILESERVER:/test
# Create an index file to be used by NGINX - Do this only once
echo "NFS Persistent Volume Test in Kubernetes is Successful!" >> $mountPoint/index.html
cat $mountPoint/index.html
# Example of success
brucelee@controller:/mnt$ cat $mountPoint/index.html
NFS Persistent Volume Test in Kubernetes is Successful!
Step 2: Create Storage Class Name ‘nfs-class’
# Check storage classes - default installation of K8 will have no custom storage classes
kim@linux01:~$ kubectl get storageclasses
No resources found
# Create custom storage class - this will fail if nfs-class has already been manually created prior, which is a desired outcome.
storageClassName=nfs-class
cat > $storageClassName.yaml <<EOF
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: $storageClassName
provisioner: kubernetes.io/nfs
reclaimPolicy: Retain
allowVolumeExpansion: true
EOF
kubectl apply -f $storageClassName.yaml
Step 3: Create a Persistent Volume
# Set variables
pvName=test-nfs-volume
storageClassName=nfs-class
storageSize=100Gi
nfsServer=192.168.80.80
nfsShare=test
# Create yammal file
cat > $pvName.yaml << EOF
apiVersion: v1
kind: PersistentVolume
metadata:
name: $pvName
spec:
storageClassName: $storageClassName
capacity:
storage: $storageSize
accessModes:
- ReadWriteMany
nfs:
path: /$nfsShare
server: $nfsServer
persistentVolumeReclaimPolicy: Retain # Other options: Recycle = rm -rf /test/* , Delete = eteled
EOF
# Apply the thing
kubectl apply -f $pvName.yaml
Step 4: Create a Persistent Volume Claim
pvClaimName=test-nfs-claim
storageClassName=nfs-class
claimSize=100Gi
cat > $pvClaimName.yaml << EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: $pvClaimName
spec:
storageClassName: $storageClassName
accessModes:
- ReadWriteMany
resources:
requests:
storage: $claimSize
EOF
kubectl apply -f $pvClaimName.yaml
Step 5: Create Deployment Plan
# Set variables
deploymentName=test-nfs-deployment
replicas=2
appName=test
imageSource=nginx:alpine
containerPort=80
containerMountPath=/usr/share/nginx/html
pvName=test-nfs-volume
pvClaimName=test-nfs-claim
# Create deployment file
cat > $deploymentName.yaml << EOF
kind: Deployment
apiVersion: apps/v1
metadata:
name: $deploymentName
spec:
replicas: $replicas
selector:
matchLabels:
app: $appName # This must be identical to the pod name (template label)
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
template:
metadata:
labels:
app: $appName
spec:
hostNetwork: true # This allows a direct ingress to any node. When this value is set, the container must not be binding to ports that are in use by the worker nodes (e.g. 53/tcp 53/udp for dns)
containers:
- name: $appName
image: $imageSource
ports:
- containerPort: $containerPort
name: $appName
volumeMounts:
- mountPath: $containerMountPath
name: $pvName # this must matches volume name
volumes:
- name: $pvName
persistentVolumeClaim:
claimName: $pvClaimName
EOF
# Apply deployment plan
kubectl apply -f $deploymentName.yaml
Step 6: Implement MetalLB Load Balancer
# Set strictARP, ipvs mode
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | sed -e "s/mode: \"\"/mode: \"ipvs\"/" | \
kubectl apply -f - -n kube-system
# Apply the manifests provided by the author, David Anderson (https://www.dave.tf/) - an awesome dude
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/metallb.yaml
# On first install only
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
# Sample output:
brucelee@controller:~$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/namespace.yaml
namespace/metallb-system created
brucelee@controller:~$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/metallb.yaml
podsecuritypolicy.policy/controller created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
role.rbac.authorization.k8s.io/pod-lister created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
rolebinding.rbac.authorization.k8s.io/pod-lister created
daemonset.apps/speaker created
deployment.apps/controller created
brucelee@controller:~$ kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
secret/memberlist created
# Customize for this system
ipRange=192.168.1.80-192.168.1.89
loadBalancerFile=metallb-config.yaml
cat > $loadBalancerFile << EOF
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- $ipRange
EOF
kubectl apply -f $loadBalancerFile
# Sample output
brucelee@controller:~$ kubectl apply -f $fileName
configmap/config created
Step 7: Create a Service Cluster
serviceName=test-service
appName=test
nodePort=30000
containerPort=80
servicePort=80
cat > test-service.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: $serviceName
spec:
type: LoadBalancer # Other options: ClusterIP, LoadBalancer
selector:
app: $appName # This name must match the template.metadata.labels.app value
ports:
- protocol: TCP
port: $servicePort
targetPort: $containerPort
# nodePort: $nodePort # optional field: by default, Kubernetes control plane will allocate a port from 30000-32767 range
EOF
kubectl apply -f test-service.yaml
clusterIP=$(kubectl get service test-service --output yaml|grep 'clusterIP: '|awk '{print $2}')
echo "clusterIP: $clusterIP"
curl $clusterIP
kubectl get service test-service
Troubleshooting
A) Pod stuck in ContainerCreating status
brucelee@controller:~$ k get pod
NAME READY STATUS RESTARTS AGE
test-nfs-deployment-54b78bc4c6-4pdz8 0/1 ContainerCreating 0 86s
test-nfs-deployment-54b78bc4c6-sgbw8 0/1 ContainerCreating 0 86s
brucelee@controller:~$ kubectl describe pods
--- Truncated for brevity ---
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 4m9s default-scheduler Successfully assigned default/test-nfs-deployment-54b78bc4c6-sgbw8 to linux03
Warning FailedMount 2m6s kubelet Unable to attach or mount volumes: unmounted volumes=[test-nfs-volume], unattached volumes=[test-nfs-volume default-token-bdhxv]: timed out waiting for the condition
Warning FailedMount 2m (x9 over 4m8s) kubelet MountVolume.SetUp failed for volume "test-nfs-volume" : mount failed: exit status 32
Mounting command: mount
Mounting arguments: -t nfs 192.168.100.21:/test /var/lib/kubelet/pods/8aa113c6-1b1e-4329-ad37-f9f04fd72e78/volumes/kubernetes.io~nfs/test-nfs-volume
Output: mount: /var/lib/kubelet/pods/8aa113c6-1b1e-4329-ad37-f9f04fd72e78/volumes/kubernetes.io~nfs/test-nfs-volume: bad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helper program.
Resolution:
- Check on the NFS server share to ensure that its been set with RW,insecure and such folder has been set with at least 750 permissions (777 preferred)
- Check the Pod Deployment template:spec:hostNetwork: true has been set
- Run this on each node: sudo apt-get install nfs-common
B) Error when the name of spec:containers:volumeMounts.name doesn't match spec:volumes:name
The Deployment is invalid: spec.template.spec.containers[0].volumeMounts[0].name: Not found:
C) Error when no storage class 'nfs-class' has NOT been defined
brucelee@controller:~$ k describe persistentvolumeclaims
Name: test-nfs-claim
Namespace: default
StorageClass: nfs
Status: Pending
Volume:
Labels: <none>
Annotations: <none>
Finalizers: [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode: Filesystem
Used By: test-nfs-deployment-6d4bff899f-5t2m2
test-nfs-deployment-6d4bff899f-kds6l
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 4m21s (x763 over 3h14m) persistentvolume-controller storageclass.storage.k8s.io "nfs" not found
Step 8: Cleanup
# Cleanup: must be in the correct sequence!
kubectl delete services test-service
kubectl delete deployment test-nfs-deployment
kubectl delete persistentvolumeclaims test-nfs-claim
kubectl delete pv test-nfs-volume