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