OKD, également connu sous le nom d’OpenShift Origin, est une solution logicielle distribuée (Go, Angular.js) implémentant un service de cloud applicatif modulaire à base de conteneurs, s’appuyant sur Kubernetes. OpenShift et Kubernetes font l’interface entre plusieurs solutions Open Source telles qu’OpenVSwitch, Dnsmasq, Docker ou Cri-O, et notamment systèmes de fichiers réseau sur lesquels nous concentrerons notre attention aujourd’hui.
Un large panel de connecteurs sont documentés, et nous ne pourrons pas tous les aborder. Les tests suivant sont réalisés sur deux installations :
- OKD 3.11, CentOS7, instances KVM 2CPU+8-16G RAM:
- Cluster GlusterFS déployé à l’aide des playbooks openshift-ansible (docker sur KVM)
- Cluster Ceph déployé à l’aide des playbooks ceph-ansible (sur KVM)
- NFS (sur KVM)
- OCP 3.7, RedHat7, instances Azure DS12_v2 / 4CPU + 27G RAM:
- RHGS 3.3, GlusterFS & Gluster-Block
- Azure-File & Azure-Disk
- NFS
NFS
NFS est une solution historique de partage de fichiers, qui présentera l’avantage de permettre à plusieurs clients l’accès et la modification simultanée d’un même système de fichiers. À ce titre, NFS permet de déclarer des volumes OpenShift “ReadWriteMany” (RWX), pouvant faciliter le déploiement d’applications distribuées. Un autre argument en faveur d’NFS pourrait être sa simplicité, en plus d’un overhead raisonnable en comparaison à tout système de fichiers distribués. Il ne faudra pourtant pas perdre de vue qu’NFS n’est pas un système de fichiers distribué et nécessitera une certaine réflection pour limiter les risques et implications d’un incident affectant un serveur NFS, en plus des considérations usuelles de lock. Enfin, un dernier frein à son adoption pourra être l’absence de “Dynamic Provisioning”. Cette fonctionnalitée permet à OpenShift de créer, allouer et purger ses volumes automatiquement. Sans elle, la déclaration d’un volume dans OpenShift doit se faire en parallèle de sa création sur le système fournissant le medium correspondant (partage NFS, Samba, …).A titre d’exemple, nous utiliserons un serveur NFS des plus traditionnels:
nfs# apt-get install lvm2 nfs-kernel-server
nfs# pvcreate /dev/sdb
nfs# vgcreate /dev/sdb nfs
nfs# lvcreate -nsample-pv -L5G nfs
nfs# mkfs.ext4 /dev/nfs/sample-pv
nfs# mkdir -p /srv/nfs/sample-pv
nfs# echo "/dev/nfs/sample-pv /srv/nfs/sample-pv ext4 defaults,noatime,nodiratime 0 0" >>/etc/fstab
nfs# mount /srv/nfs/sample-pv
nfs# chown nobody:nogroup /srv/nfs/sample-pv
nfs# chmod 777 /srv/nfs/sample-pv
nfs# echo "/srv/nfs/sample-pv *(rw,root_squash)" >>/etc/exports
nfs# exportfs -a
Nous pouvons maintenant déclarer notre “Persistent Volume” (PV) dans OpenShift :
master# cat sample-pv.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-sample-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
claimRef:
namespace: myproject
name: nfs-sample-pvc
nfs:
path: /srv/nfs/sample-pv
server: nfs.example.com
persistentVolumeReclaimPolicy: Recyclemaster
# oc create -f sample-pv.yml
Sur nos noeuds OpenShift, il faudra reconfigurer un booleen SELinux :
ocpX# setsebool -P virt_use_nfs 1
À ce stade, on peut créer une “Persistent Volume Claim” (PVC).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-sample-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: nfs-storage
Et finalement, monter ce volume dans un conteneur, depuis une “DeploymentConfig” :
apiVersion: v1
kind: DeploymentConfig
metadata:
name: subsonic
spec:
replicas: 1
selector:
name: subsonic
strategy:
type: Recreate
template:
metadata:
labels:
name: subsonic
spec:
containers:
- capabilities: {}
[...]
volumeMounts:
- mountPath: /var/music
name: subsonic-caches
[...]
volumes:
- name: subsonic-caches
persistentVolumeClaim:
claimName: nfs-sample-pvc
GlusterFS
GlusterFS est une solution de stockage distribuée relativement populaire. La partie serveur peut admettre une réplication des données sur de longues distances, ou plus généralement la mise en commun de plusieurs volumes pour y distribuer et répliquer des volumes virtuels, aux quels l’on accèdera en utilisant un client fuse. De même qu’NFS, GlusterFS permettra à plusieurs clients de manipuler un même système de fichiers simultanément, ce qui en fait un choix de prédilection dans un contexte Kubernetes. Le Dynamic Provisioning repose sur Heketi, offrant une interface web RESTful permettant le controle du cycle de vie de volumes GlusterFS, assurant une intégration presque parfaite. Coté points négatifs, il conviendra de mentionner de mauvaises performances sur les petites lectures et écritures, de possibles erreurs lors de la suppression de volumes pouvant induire la non-supression de volumes logiques qui ne sont pourtant plus utilisés par gluster, et donc une “perte” d’espace souvent asymétrique, ou encore une certaine vulnérabilité aux split-brain – qu’il faudra résoudre, ce processus n’étant pas automatisé. Admettant l’utilisation des playbooks openshift-ansible pour le déploiement de GlusterFS, une StorageClass glusterfs-storage aura été déclaré. Il sera également possible de rattacher une intallation OpenShift à un cluster GlusterFS, idéalement par l’intermédiaire d’Heketi. Il faudra ensuite créer un Endpoint et une StorageClass :
master# cat gluster-endpoints.yml
apiVersion: v1
kind: Endpoints
metadata:
name: glusterfs-cluster
subsets:
- addresses:
- ip: 192.168.42.220
ports:
- port: 1
- addresses:
- ip: 192.168.42.221
ports:
- port: 1
- addresses:
- ip: 192.168.42.222
ports:
- port: 1
master# oc create -f gluster-endpoints.yml
master# cat glusterfs-sc.yml
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: glusterfs
provisioner: kubernetes.io/glusterfs
parameters:
endpoint: "glusterfs-cluster"
resturl: "http://192.168.42.42:8080"
restuser: "ocp"
restuserkey: "secret"
master# oc create -f glusterfs-sc.yml
Afin de permettre à OpenShift l’usage de GlusterFS pour toute demande de volume persistAnt, on peut ensuite éditer notre StorageClass en y rajoutant une annotation :
oc edit storageclass glusterfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
À défaut, il sera toujours possible d’éditer vos Persistent Volume Claim pour réclamer leur espace depuis une classe spécifique :
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-extensions
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: sonarqube-extensions
Gluster-Block
Si GlusterFS présente certains inconvénients en termes de performances, il est possible de configurer un cluster Gluster pour servir des volumes block par l’intermédiaire d’un service Gluster-Block, et l’utilisation d’un client iSCSI. Les avantages d’une telle solution restent limités aujourd’hui, souffrant de bugs de jeunesse – empéchant notament l’extension des volumes de type block – ou d’une architecture déroutante, et à nouveau prone aux incidents à base de split brains. Quand bien même le gain en performances sur les petites opérations est indéniable, tandis que l’adoption d’un standard tel qu’iSCSI est bienvenue. Contrairement à GlusterFS, et de même que toute solution fournissant un périphérique block, Gluster-Block ne permettra pas la mise à disposition de volumes accessibles par plusieurs clients simultanément. Il faudra s’assurer que vos DeploymentConfig ne dépendent pas de fonctionallitées telles que le partage de volume entre plusieurs conteneurs ou les stratégies de déploiement “Rolling”. La configuration du Dynamic Provisioning demandera, en plus d’Heketi, la mise en place d’un service supplémentaire, pouvant se déployer dans un projet d’OpenShift,. Notez que ce service dépendra lui-même d’Heketi pour reconfigurer votre cluster GlusterFS fonction des besoins d’OpenShift.
kind: List
apiVersion: v1
items:
- kind: ClusterRole
apiVersion: v1
metadata:
name: glusterblock-provisioner-runner
labels:
glusterfs: block-provisioner-runner-clusterrole
glusterblock: provisioner-runner-clusterrole
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "delete"]
- apiGroups: [""]
resources: ["routes"]
verbs: ["get", "list"]
- apiVersion: v1
kind: ServiceAccount
metadata:
name: glusterblock-provisioner
labels:
glusterfs: block-provisioner-sa
glusterblock: provisioner-sa
- apiVersion: v1
kind: ClusterRoleBinding
metadata:
name: glusterblock-provisioner
roleRef:
name: glusterblock-provisioner-runner
subjects:
- kind: ServiceAccount
name: glusterblock-provisioner
namespace: storage-project
- kind: DeploymentConfig
apiVersion: v1
metadata:
name: glusterblock-provisioner-dc
labels:
glusterfs: block-provisioner-dc
glusterblock: provisioner-dc
annotations:
description: Defines how to deploy the glusterblock provisioner pod.
spec:
replicas: 1
selector:
glusterfs: block-provisioner-pod
triggers:
- type: ConfigChange
strategy:
type: Recreate
template:
metadata:
name: glusterblock-provisioner
labels:
glusterfs: block-provisioner-pod
spec:
serviceAccountName: glusterblock-provisioner
containers:
- name: glusterblock-provisioner
image: ' '
imagePullPolicy: IfNotPresent
env:
- name: PROVISIONER_NAME
value: gluster.org/glusterblock
triggers:
- imageChangeParams:
automatic: true
containerNames:
- glusterblock-provisioner
from:
kind: ImageStreamTag
name: rhgs-gluster-block-prov-rhel7:latest
namespace: openshift
lastTriggeredImage: ""
type: ImageChange
- type: ConfigChange
master# oc create -n storage-project -f gluster-block-provisioner.yml
Une fois ce template instancié, il sera possible de déclarer une StorageClasse utilisant notre service de provisioning :
master# cat gluster-block-sc.yml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gluster-block
provisioner: gluster.org/glusterblock
parameters:
hacount: "3"
resturl: "http://192.168.42.42:8080"
restuser: "ocp"
restuserkey: "secret"
master# oc create -n gluster-block-sc.yml
Notez que nos paramètres font ici toujours référence au service Heketi. Le provisioner Gluster Block n’hébergeant aucun Service. La configuration des noeuds OCP pour usage de volumes iSCSI reste des plus classique :
ocpX# yum install iscsi-initiator-utils device-mapper-multipath
ocpX# cat /etc/multipath.conf
devices {
device {
vendor "LIO-ORG"
user_friendly_names "yes"
path_grouping_policy "failover"
path_selector "round-robin 0"
failback immediate
path_checker "tur"
prio "const"
no_path_retry 120
rr_weight "uniform"
}
}
ocpX# systemctl enable multipathd
ocpX# systemctl start multipathd
Après quoi, vos templates OpenShift pourront faire l’usage de cette nouvelle classe :
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
storageClassName: gluster-block
Ceph-RBD
En ce qui concerne les solutions blocks distribuées, Ceph est le standard Open Source. A l’instar de Gluster Block, Ceph RBD ne permettra pas l’utilisation d’un même volume par plusieurs conteneurs simultanément. Si l’on peut regretter l’absence de playbooks déployant un cluster Ceph dans la cuisine OpenShift, ceux de Sebastien Han feront l’affaire. Ayant déployé un cluster Ceph, nous devrons installer le client Ceph sur nos noeuds OpenShift, et y installer le même fichier de configuration que sur nos noeuds Ceph :
[global]
fsid = xxx-yy-zzz
mon host = 10.42.253.110,10.42.253.111,10.42.253.112
public network = 10.42.253.0/24
cluster network = 10.42.253.0/24
[client.libvirt]
admin socket = /var/run/ceph/$cluster-$type.$id.$pid.$cctid.asok
log file = /var/log/ceph/qemu-guest-$pid.log
Depuis un noeud Ceph pouvant administrer votre cluster, il faudra ensuite créer un pool, un utilisateur, exporter son keyring et celui du client admin :
monX# ceph osd pool create kube 128
monX# ceph auth get-or-create client.kube mon 'allow r' osd 'allow class-read object_prefix rbd_children, allow rwx pool=kube' -o ceph.client.kube.keyring
monX# ceph auth get-key client.admin | base64
CLIENT_ADMIN_KEYRING_B64
monX# ceph auth get-key client.kube | base64
CLIENT_KUBE_KEYRING_B64
Il sera alors possible de configurer OpenShift pour créer ses volumes dans notre pool Ceph :
master# cat ceph-admin-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: ceph-admin-secret
namespace: kube-system
type: kubernetes.io/rbd
data:
key: CLIENT_ADMIN_KEYRING_B64
master# oc create -f ceph-admin-secret.yml
master# cat ceph-user-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: ceph-user-secret
namespace: kube-system
type: kubernetes.io/rbd
data:
key: CLIENT_KUBE_KEYRING_B64
master# oc create -f ceph-user-secret.yml
master# cat ceph-sc.yml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ceph-storage
provisioner: ceph.com/rbd
parameters:
monitors: 10.42.253.110:6789,10.42.253.111:6789,10.42.253.112:6789
adminId: admin
adminSecretName: ceph-admin-secret
adminSecretNamespace: kube-system
pool: kube
userId: kube
userSecretName: ceph-secret-user
userSecretNamespace: kube-system
Ici, pas besoin de service dédié au Dynamic Provisioning, OpenShift saura manipuler notre cluster Ceph sans plus de configurations. À nouveau, nous pourrons forcer la crération de nos volumes depuis notre PVC, ou configurer notre StorageClass avec l’annotation is-default.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: ceph-storage
Azure-File
Azure-File est un connecteur qui, comme son nom l’indique, repose sur un service Azure de partages Samba. Celui-ci est relativement similaire à NFS, sachant qu’il permettra la définition de volumes que plusieurs clients pourront monter en parallèle, tandis que le Dynamique Provisioning n’est pas supporté et qu’il faudra donc créer nos partages depuis la console ou tout client de l’API Azure. Il faudra en revanche signaler que les options de montage de volumes Azure-File feront qu’il ne sera pas possible d’y créer de lien symbolique, tandis que les permissions présentées seront forcées à 777, … Certains applicatifs (tels que Jenkins) refuseront de se lancer sur ce genre de volumes. Pour n’avoir eu accès qu’à des partages de type “Standard_LRS”, il faut admettre que les débits observés ne sont pas impressionnants – sachant qu’il existerait des partages de type “Premium_LRS”. Conscient de ces détails, l’utilisation de volumes Azure-File peut faire du sens, notament en substitution d’un serveur NFS, offrant des volumes RWX, retirant la charge de l’hébergement d’un service de stockage de données distribué et hautement disponible, virtuellement sans limite de stockage. La configuration d’OpenShift pour monter ses volumes depuis un partage Azure-File demandera la création d’un Secret et d’une StorageClass :
master# cat azure-storage01-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: azure-storage01-secret
type: Opaque
data:
azurestorageaccountname: storage01
azurestorageaccountkey: secretString==
master# oc create -f azure-storage01-secret.yml
master# cat azurefile-sc.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azurefile-storage
provisioner: kubernetes.io/azure-file
parameters:
skuName: Standard_LRS
location: northeurope
storageAccount: storage01
master# oc create -f azurefile-sc.yml
Pour permettre l’accès à ces partages, il faudra également installer le client samba sur vos noeuds OpenShift :
ocpX# yum install samba-client samba-common cifs-utils
ocpX# setsebool -P virt_use_samba on
ocpX# setsebool -P virt_sandbox_use_samba on
Il est alors possible de définir vos Persistent Volume, associant notre Secret à un partage préalablement créé depuis la console Azure :
master# cat azurefile-storage01-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: azurefile-storage01-pv
spec:
accessModes:
- ReadWriteMany
azureFile:
secretName: azure-storage01-secret
shareName: storage01
readOnly: false
claimRef:
namespace: myproject
name: azurefile-storage01-pvc
capacity:
storage: 10Gi
storageClassName: azurefile-storage
master# oc create -f azurefile-storage01-pv.yml
Enfin, nous pourrons créer une PVC correspondant à la claimRef évoqué dans notre PV, pour ré-utilisation dans une DeploymentConfig :
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: azurefile-storage01-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: azurefile-storage
Azure-Disk
De même qu’Azure-File, Azure-Disk est un service reposant sur l’offre d’Azure. Il est ici question de périphériques block comparables aux volumes Gluster Block ou Ceph RBD. OpenShift communique avec l’API d’Azure sans nécessiter la présence de service supplémentaire tel qu’Heketi, les volumes provisionnés dynamiquement sont rattaches à nos instances Azure au niveau de l’hyperviseur, de manière transparante pour les noeuds OpenShift. Le support du Dynamic Provisioning est relativement récent dans OpenShift, et toujours sujet à bugs, pouvant laisser vos instances Azure dans un état second où rattacher et détacher des volumes n’est plus possible. La suppression de volumes créés dynamiquement depuis OpenShift peut ne pas purger l’image correspondante de vos resources Azure, si celles-si font l’objet de backups : il faudra d’aborder en purger les snapshots. Chaque nouvelle version livre son lot de correctifs sur le sujet, et sans doute bientôt verrons-nous une implémentation fiable. À défaut, cette-ci restera néamoins des plus intéressantes dans un context Azure, où l’implémentation d’un cluster Gluster ou Ceph demandera un investissement conséquent en instances et volumes Azure, le maintien en conditions opérationnelles du service, son monitoring, … Pour des performances qui ne dépasseront pas celles des disques servant de support, à savoir des volumes comparables à ceux manipulés avec le driver Azure-Disk d’OpenShift. Pour permettre à OpenShift de communiquer avec Azure, il nous faudra une clé d’API que nous inclurons dans un fichier sur tous nos noeuds :
openshift# cat /etc/azure/azure.conf
tenantId: tenId
subscriptionId: subId
aadClientId: tenId
aadClientSecret: tenSecret
aadTenantId: tenId
resourceGroup: resourceGroup
location: mylocation
Sur nos noeuds master, il faudra ensuite éditer le fichier /etc/origin/master/master-config.yml, modifier la définition d’apiServerArguments et controllerArguments :
master# grep -EA4 '(apiServer|controller)Arguments:' /etc/origin/master/master-config.yml
apiServerArguments:
cloud-provider:
- azure
cloud-config:
- /etc/azure/azure.conf
controllerArguments:
cloud-provider:
- azure
cloud-config:
- /etc/azure/azure.conf
master# systemctl restart atomic-openshift-master-api
master# systemctl restart atomic-openshift-master-controllers
Sur tous vos noeuds OpenShift, il faudra enfin éditer le fichier /etc/origin/node/node-config.yml, modifier la défitinion de kubeletArguments :
ocpX# grep -A4 kubeletArguments: /etc/origin/node/node-config.yml
kubeletArguments:
cloud-provider:
- azure
cloud-config:
- /etc/azure/azure.conf
ocpX# systemctl restart atomic-openshift-node
Notez que le redémarrage du service OpenShift node, dans le cas où l’on configure un cloud-provider, va d’abord supprimer l’enregistrement de votre noeud du cluster, pour le re-créer, avec pour seul labels ceux fournis par Azure ou définis dans votre node-config. Tout label ayant été rajouté en cours de route (tel que node-exporter dans le cadre de collection de métriques Prometheus, ou logging pour la collection des logs de vos noeuds OpenShift) pourrait manquer après cette manipulation. N’hésitez pas à exporter la définition de vos noeuds avant manipulation, pour comparaison après redémarrage d’openshift-node.
oc export node my-hostname >previous-hostname-data
Nous devrions ensuite pouvoir définir une StorageClass azure-disk :
master# cat azuredisk-sc.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azuredisk-storage
provisioner: kubernetes.io/azure-disk
parameters:
skuName: Premium_LRS
storageaccounttype: Premium_LRS
kind: Shared
location: northeurope
storageAccount: storage01
master# oc create -f azuredisk-sc.yml
À nouveau, l’utilisation de cette StorageClass pourra se faire depuis nos PVC :
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: azuredisk-storage