Présentation de Torus, un système de fichier distribué cloud natif

Cet article est la partie 2 de la serie "Kubernetes deep dive" :

  1. Déployer un cluster Kubernetes sur AWS avec kube-aws
  2. Présentation de Torus, un système de fichier distribué cloud natif
  3. Kubernetes : Introduction aux PetSet et bootstrap d'un cluster Consul
  4. Kubernetes : Ingress Controller avec Træfɪk et Let's Encrypt
thumbernail Kubernetes

coreos

On continue dans la série Kubernetes avec un article sur Torus, un système de fichier distribué cloud-natif développé par CoreOS qui offre un stockage redondant et persistant aux pods Kubernetes.

Qu'est-ce que Torus ?

Dans l'écosystème conteneurs, le stockage persistant reste un problème majeur : les applications stateless se conteneurisent facilement et n'ont généralement pas besoin de données persistantes. Ce n'est pas le cas des applications stateful telles que les bases de données par exemple. Dans l'écosystème Docker, il existe déjà des plugins de stockage basés sur des technologies diverses :

  • GlusterFS
  • NFS
  • Convoy (qui présente GlusterFS ou NFS (ainsi que d'autres backends) à Docker)

Du côté des volumes Kubernetes, de nombreux backends sont supportés :

  • iSCSI
  • GlusterFS
  • rbd (Ceph)
  • NFS
  • Volumes basés sur le Cloud Provider (GCE PD, AWS EBS, Azure Volume)

À l'exception des backends Cloud Providers, la plupart peuvent être relativement lourds à mettre en place (comme Ceph), ou à scaler dans une infrastructure cloud native orientée conteneurs et constituée d'un nombre conséquent de "petites" machines (GlusterFS/NFS).

En juin 2016, CoreOS a annoncé la création d'un nouveau système de fichier distribué : Torus, qui a pour but de fournir du stockage distribué cloud natif.

Torus

Dans la philosophie micro services et GIFEE (Google Infrastructure For Everyone Else) de CoreOS, Torus, se concentre sur le stockage ; les parties metadata et consensus sont déléguées à etcd : un key/value store hautement scalable utilisé en production par des solutions comme Kubernetes et Docker Swarm. Torus supporte ainsi les fonctionnalités classiques des systèmes de fichiers distribués telles que la réplication ou la répartition équilibrée des données.

Quelles sont les différences avec GlusterFS ou NFS ? Torus fournit pour le moment uniquement un stockage de type bloc via le protocole nbd (Network Block Device). De part son architecture, il sera possible ensuite d'exposer d'autres types de stockage notamment de l'objet ou encore du système de fichiers (comme NFS ou GlusterFS). Torus est également écrit en Go, compilé statiquement, ce qui le rend facilement portable et conteneurisable. Torus se décompose en 3 binaires :

  • torusd : le démon Torus
  • torusctl : contrôle du cluster et des volumes
  • torusblk : contrôle du montage/démontage des volumes bloc

Intégration à Kubernetes : Flex Volume

Torus est indépendant de Kubernetes. Il peut s'installer de manière autonome et exposer ses volumes via le protocole NDB. Ces volumes peuvent ensuite être utilisés comme n'importe quel volume bloc.

Kubernetes, en plus du support de volumes built-in, dispose d'un plugin FlexVolume qui offre aux solutions de stockage tierce partie la possibilité d'implémenter un driver sans toucher au cœur de Kubernetes. Le binaire torusblk est compatible avec les spécifications FlexVolume de Kubernetes, ce qui permet de monter directement des volumes Torus dans des pods.

Test sur un cluster Kubernetes : déploiement d'etherpad-lite

Ce test est réalisé sur un cluster Kubernetes 1.3, composé de 3 nœuds CoreOS dont 1 contrôleur. Nous allons déployer Torus directement via Kubernetes. Pour fonctionner, Torus a besoin : - Du module kernel nbd sur les workers Kubernetes - Du binaire torusblk installé dans le répertoire des drivers FlexVolume spécifié dans la configuration du Kubelet - De etcd en version 3 - Du stockage disponible sur les workers Kubernetes

Installation du plugin

Les binaires de Torus sont disponibles ici.

Tout d'abord on vérifie la configuration du Kubelet :

# /etc/systemd/system/kubelet.service
[Service]
ExecStartPre=/usr/bin/mkdir -p /etc/kubernetes/manifests

Environment=KUBELET_VERSION=v1.3.0_coreos.0
ExecStart=/usr/lib/coreos/kubelet-wrapper \
  --api-servers=http://127.0.0.1:8080 \
  --network-plugin-dir=/etc/kubernetes/cni/net.d \
  --network-plugin= \
  --register-schedulable=false \
  --allow-privileged=true \
  --config=/etc/kubernetes/manifests \
  --hostname-override=192.168.122.110 \
  --cluster-dns=10.3.0.10 \
  --cluster-domain=cluster.local \
  --volume-plugin-dir="/etc/kubernetes/kubelet-plugins/volume/exec/"
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

Il faut rajouter la ligne volume-plugin-dir si celle-ci n'est pas présente. Le chemin par défaut du Kubelet est --volume-plugin-dir="/usr/libexec/kubernetes/kubelet-plugins/volume/exec/" mais /usr est en lecture seule sur CoreOS.

Le répertoire spécifié doit être ensuite créé, avant de copier le binaire torusblk en le renommant torus sur tous les workers du cluster dans /etc/kubernetes/kubelet-plugins/volume/exec/coreos.com~torus/ :

ansible -i ../ansible-coreos/inventory "worker*" -m file -b -a "dest=/etc/kubernetes/kubelet-plugins/volume/exec/coreos.com~torus mode=700 state=directory"
ansible -i ../ansible-coreos/inventory "worker*" -m copy -b -a "src=/home/klefevre/go/src/github.com/coreos/torus/bin/torusblk dest=/etc/kubernetes/kubelet-plugins/volume/exec/coreos.com~torus/torus mode=700"

Torus a également besoin du module kernel nbd. Il est possible de le charger à la volée :

ansible -i ../ansible-coreos/inventory -m shell -b -a "modprobe nbd" "worker*"

Ou par exemple pour CoreOS de le charger via cloud-init au démarrage :

#cloud-config
write_files:
  - path: /etc/modules-load.d/nbd.conf
    content: nbd
coreos:
  units:
    - name: systemd-modules-load.service
      command: restart

Il faut ensuite redémarrer le Kubelet sur tous les nœuds et c'est terminé pour la partie pré-requis. Le reste se déroule uniquement sur Kubernetes :

ansible -i inventory -m shell -b -a "systemctl restart kubelet" "worker*"

Déploiement d'etcdv3

Pour fonctionner, Torus a besoin d'etcd en version 3 minimum, le service peut être déployé simplement sur Kubernetes et publié à l'aide d'un service :

# etcdv3-daemonset.yml
---
apiVersion: v1
kind: Service
metadata:
  labels:
    name: etcdv3
  name: etcdv3
spec:
  type: NodePort
  clusterIP: 10.3.0.100
  ports:
    - port: 2379
      name: etcdv3-client
      targetPort: etcdv3-client
  selector:
    daemon: etcdv3
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: etcdv3
  labels:
    app: etcdv3
spec:
  template:
    metadata:
      name: etcdv3
      labels:
        daemon: etcdv3
    spec:
      containers:
      - name: etcdv3
        image: quay.io/coreos/etcd:latest
        imagePullPolicy: Always
        ports:
        - name: etcdv3-peers
          containerPort: 2380
        - name: etcdv3-client
          containerPort: 2379
        volumeMounts:
        - name: etcdv3-data
          mountPath: /var/lib/etcd
        - name: ca-certificates
          mountPath: /etc/ssl/certs
        env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: ETCD_DATA_DIR
          value: /var/lib/etcd
        - name: ETCD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: ETCD_INITIAL_ADVERTISE_PEER_URLS
          value: http://$(POD_IP):2380
        - name: ETCD_ADVERTISE_CLIENT_URLS
          value: http://$(POD_IP):2379
        - name: ETCD_LISTEN_CLIENT_URLS
          value: http://0.0.0.0:2379
        - name: ETCD_LISTEN_PEER_URLS
          value: http://$(POD_IP):2380
        - name: ETCD_DISCOVERY
          value: https://discovery.etcd.io/         #-> url de discovery à générer avant déploiement
      volumes:
      - name: etcdv3-data
        hostPath:
          path: /srv/etcdv3
      - name: ca-certificates
        hostPath:
          path: /usr/share/ca-certificates/

L'utilisation d'un DaemonSet permet de déployer une instance d'etcd par worker. Sur l'un des nœuds l'état du cluster peut être vérifié avec etcdctl via le service publié :

kubectl create -f etcdv3-daemonset.yml

kubectl get pods --selector="daemon=etcdv3"
NAME           READY     STATUS    RESTARTS   AGE
etcdv3-4jncl   1/1       Running   1          1d
etcdv3-7r46s   1/1       Running   1          1d
etcdv3-o0n86   1/1       Running   1          1d
core@coreos00 ~ $ etcdctl --endpoints=http://10.3.0.100:2379 cluster-health
member 3f87be33d946732d is healthy: got healthy result from http://10.2.78.2:2379
member a19d841707579e60 is healthy: got healthy result from http://10.2.36.3:2379
member d5479de5c3342460 is healthy: got healthy result from http://10.2.55.3:2379
cluster is healthy

core@coreos00 ~ $ etcdctl --endpoints=http://10.3.0.100:2379 member list
3f87be33d946732d: name=etcdv3-7r46s peerURLs=http://10.2.78.2:2380 clientURLs=http://10.2.78.2:2379 isLeader=true
a19d841707579e60: name=etcdv3-4jncl peerURLs=http://10.2.36.3:2380 clientURLs=http://10.2.36.3:2379 isLeader=false
d5479de5c3342460: name=etcdv3-o0n86 peerURLs=http://10.2.55.2:2380 clientURLs=http://10.2.55.3:2379 isLeader=false

Déploiement de Torus

Torus est aussi déployé directement sur Kubernetes, toujours via un DaemonSet qui permet d'avoir une instance de Torus par worker. Les blocs seront stockés dans un volume de l'hôte.

# torus-daemonset.yml
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: torus
  labels:
    app: torus
spec:
  template:
    metadata:
      name: torus
      labels:
        daemon: torus
    spec:
      containers:
      - name: torus
        image: quay.io/coreos/torus:latest
        imagePullPolicy: Always
        ports:
        - name: torus-peer
          containerPort: 40000
        - name: torus-http
          containerPort: 4321
        env:
        - name: ETCD_HOST
          value: 10.3.0.100                         #-> Cluster
        - name: STORAGE_SIZE
          value: 5GiB                               #-> Taille maximum donnée au pool de stockage par worker
         - name: LISTEN_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: AUTO_JOIN
          value: "1"
        - name: DEBUG_INIT
          value: "1"
        - name: DROP_MOUNT_BIN
          value: "0"
        volumeMounts:
          - name: torus-data
            mountPath: /data
      volumes:
        - name: torus-data
          hostPath:
            path: /srv/torus                        #-> repertoire de l'host contenant les données des volumes Torus
kubectl create -f etcdv3-daemonset.yml

kubectl get pods --selector="daemon=torus"
NAME          READY     STATUS    RESTARTS   AGE
torus-56p79   1/1       Running   0          1m
torus-87f9t   1/1       Running   0          1m
torus-wc54r   1/1       Running   0          1m

Sur la machine qui contrôle Kubernetes, le cluster peut être contrôlé à l'aide des binaires Torus et d'un port forward vers un pod etcd :

kubectl port-forward etcdv3-4jncl

Il est ensuite possible de contrôler Torus localement :

torusctl list-peers
Handling connection for 2379
ADDRESS                 UUID                                  SIZE     USED  MEMBER  UPDATED        REB/REP DATA
http://10.2.55.2:40000  dce8cf72-45f5-11e6-9426-02420a023702  5.0 GiB  0 B   OK      4 seconds ago  0 B/sec
http://10.2.78.3:40000  dd53432a-45f5-11e6-8fec-02420a024e03  5.0 GiB  0 B   OK      4 seconds ago  0 B/sec
http://10.2.36.4:40000  dd800d8c-45f5-11e6-b812-02420a022404  5.0 GiB  0 B   OK      2 seconds ago  0 B/sec
Balanced: true Usage:  0.00%

Les 3 instances de Torus avec chacune 5 GiB dans le pool de stockage sont disponibles. Il faut ensuite créer un volume de 1 GiB pour Etherpad :

torusctl volume create-block pad 1GiB
torusctl volume list
Handling connection for 2379
VOLUME NAME  SIZE     TYPE
pad          1.0 GiB  block

Le volume est maintenant disponible et utilisable dans Kubernetes.

Déploiement d'Etherpad

Etherpad est déployé simplement via un deployment et un service :

# deployment-etherpad.yml
---
apiVersion: v1
kind: Service
metadata:
    name: etherpad
    labels:
        app: etherpad
spec:
    selector:
        app: etherpad
    ports:
        - port: 80
          targetPort: etherpad-port
    type: NodePort
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: etherpad
  labels:
    app: etherpad
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: etherpad
    spec:
      containers:
      - name: etherpad
        image: "osones/etherpad:alpine"
        ports:
        - name: etherpad-port
          containerPort: 9001
        volumeMounts:
        - name: etherpad
          mountPath: /etherpad/var
        env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
      volumes:
        - name: etherpad
          flexVolume:
            driver: "coreos.com/torus"              #-> le driver pour le FlexVolume
            fsType: "ext4"
            options:
              volume: "pad"                         #-> Le nom du volume créé precedemment
              etcd: "10.3.0.100:2379"               #-> L'adresse du service etcd créé precedemment

Vérification du déploiement :

kubectl create -f ymlfiles/deployment-etherpad.yml
deployment "etherpad" created
service "etherpad" created

kubectl describe service etherpad
Name:                   etherpad
Namespace:              default
Labels:                 app=etherpad
Selector:               app=etherpad
Type:                   NodePort
IP:                     10.3.0.157
Port:                   <unset> 80/TCP
NodePort:               <unset> 31594/TCP
Endpoints:              10.2.55.4:9001
Session Affinity:       None
No events.

Pour des raisons de simplicité et en fonction de la configuration initiale, le service écoute également sur un NodePort, ce qui permet de joindre le service en accédant à n'importe quel worker sur le port 31594.

Pour tester la persistance des données, nous allons créer un pad nommé torus :

etherpad-pod1

L'intérêt est de marquer le nœud sur lequel fonctionne actuellement Etherpad comme non-schedulable (cordon dans les termes Kubernetes) puis de détruire le pod Etherpad afin de le rescheduler sur un autre nœud :

kubectl describe pods etherpad-2266423034-6wk4u
Name:           etherpad-2266423034-6wk4u
Namespace:      default
Node:           192.168.122.111/192.168.122.111
Start Time:     Sat, 09 Jul 2016 19:11:23 +0200
Labels:         app=etherpad
                pod-template-hash=2266423034
Status:         Running
IP:             10.2.55.4
Controllers:    ReplicaSet/etherpad-2266423034
...

kubectl cordon 192.168.122.111
node "192.168.122.111" cordoned

kubectl get nodes
NAME              STATUS                     AGE
192.168.122.110   Ready                      4d
192.168.122.111   Ready,SchedulingDisabled   4d
192.168.122.112   Ready                      4d

Le scheduling est bien désactivé sur le nœud 192.168.122.111. On détruit le pod Etherpad qui sera ensuite relancé automatiquement grâce au replicaset défini dans le déploiement :

kubectl get pods --selector="app=etherpad"
NAME                        READY     STATUS    RESTARTS   AGE
etherpad-2266423034-6wk4u   1/1       Running   0          14m

kubectl delete pods etherpad-2266423034-6wk4u
pod "etherpad-2266423034-6wk4u" deleted

Vérification du nouveau pod :

kubectl get pods --selector="app=etherpad"
NAME                        READY     STATUS    RESTARTS   AGE
etherpad-2266423034-urb2f   1/1       Running   0          1m

kubectl describe pods --selector="app=etherpad"
Name:           etherpad-2266423034-urb2f
Namespace:      default
Node:           192.168.122.112/192.168.122.112
Start Time:     Sat, 09 Jul 2016 19:25:46 +0200
Labels:         app=etherpad
                pod-template-hash=2266423034
Status:         Running
IP:             10.2.36.5
Controllers:    ReplicaSet/etherpad-2266423034

etherpad-pod2

Bon, j'avoue que pour les captures d'écran, je vous invite à vous baser sur la confiance.

Conclusion

Torus est un produit très jeune qui vient s'inscrire dans la philosophie des autres produits open source lancés par CoreOS.

Si le stockage bloc a effectivement peu d'intérêt lors de l'utilisation d'un Cloud Provider puisque Kubernetes supporte déjà les PD sur GCE et les volumes EBS sur AWS, Torus va en revanche permettre, dans le cas d'une utilisation on premises ou de Cloud Providers différents, d'abstraire et d'agréger les volumes de multiples instances.

Enfin, en ce qui concerne GlusterFS et NFS, Torus ne joue pas encore sur le même terrain puisque seul le stockage bloc est aujourd'hui disponible.

Reste à voir non seulement comment le produit évolue mais également si l'engouement est au rendez-vous comme il l'a été pour CoreOS et leurs autres produits.

Kevin Lefevre

Découvrez les technologies d'alter way