---
Title: Kubernetes et ceph sont dans un bateau
Subtitle: - Infrastructure pour Pair2Jeux
date: 2019-01-15 10:33
Authors: Victor
Slug: peertube-k8s-ceph
Category: Système
Tags: kubernetes, k8s, ceph, peertube, haute disponibilité
keywords: kubernetes, k8s, ceph, peertube, haute disponibilité
Status: published
---
[TOC]

# De quoi va-t-on parler aujourd'hui

## Mise en contexte
Car oui, il faut du contexte !

Je gère depuis maintenant un an **[une instance](https://videos.pair2jeux.tube) utilisant [Peertube](https://joinpeertube.org/en/)** pour héberger des vidéos dédiées au monde vidéoludique francophone.

A l'origine mis en place pour aider au projet et tester la version beta sur un petit serveur seul dans son coin, les limites se sont assez vite faite sentir quand les gens intéressés par le projet ont commencés à devenir plus nombreux (et c'est cool !)
Le **stockage** notamment est une grosse problématique pour **les vidéos**, et c'est ce point qui a vite présenté un goulot d'étranglement.
Pour proposer un service de qualité, je voulais utiliser un **stockage réparti** ou à minima sécurisé, car les sauvegardes d'une telle quantité de données ne sont pas sans poser également quelques problèmes.  Techniquement, il me fallait donc du **RAID en mono-serveur**, ou un **stockage de type distribué** (ceph, glusterfs, moosefs, ...)


Malheureusement en cherchant de meilleurs solution de stockage, il est assez vite apparu que les solutions de type Amazon S3 étaient les plus répandues, extensibles, garanties, etc.. Et surtout, totalement peu sûre et chez des GAFAM, chose que je voulais **éviter absolument** pour un projet dont l'optique est de proposer une alternative à Youtube !

De même, les **stockages distribués** plus standards que l'on peut trouver chez Scaleway, OVH ou Hetzner **coutent une fortune** dès que l'on veut de la **quantité** (au dessus de 500Go, préparez l'héritage pour le payer).

Associé au fait que j'aime bien faire moi-même les choses, j'ai donc décidé de **partir sur une infrastructure maison**, en place maintenant depuis 6 mois, et c'est celle-ci que je vais maintenant vous proposer :-)

## Mise en technique
Mes **besoins techniques** étaient donc les suivants :

 - [Kubernetes](https://kubernetes.io/) pour sa capacité à héberger en cluster des images dockers (et donc les différents outils nécessaire à Peertube)
 - [Ceph](https://ceph.com/) pour le stockage, bien connu, plutôt robuste avec de très bonnes performances et supporté par Kubernetes
 - Un réseau dédié au stockage et au trafic interne de Kubernetes, donc un réseau privé (dans l'idéal chiffré) avec un bon débit
 - Des serveurs (3 pour commencer) pour faire tourner tout ça, disposant d'un SSD pour le système et d'un disque dur que l'on dédiera au stockage Ceph
 - Des serveurs disposant au minimum de 16Go de RAM et d'un CPU i7 pour faire tourner Peertube tout en tenant la charge supplémentaire de Kubernetes et Ceph

Ceux qui sont un peu familier avec les offres de serveur privés existantes savent que pour les serveurs notamment, c'est une configuration très difficile à trouver.
Pour des questions de commodité les hébergeurs proposent en effet des serveurs proposant soit peu de disque en SSD, soit de gros volumes de stockage en HDD, mais pas de mix des deux.

C'est pourquoi je vous propose ici un **compromis**, avec des **hypothèses simplificatrices** qui présentent des risques mais que j'ai décidé d'accepter, on en parlera à la fin.

 - 3 serveurs chez [Hetzner](https://www.hetzner.com/sb?country=fr) (qui propose des serveurs peu cher avec un trafic mensuel très correct)
 - 2 disques HDD de 3To par serveur, core i7, 16Go de RAM
 - Un réseau VPN privé monté entre les serveurs pour gérer le trafic Kubernetes et Ceph
 - Un des disques sera entièrement dédié à Ceph. L'autre permettra sur une partition de 100Go d'installer le système, le reste devant à terme être intégré à ceph

Nous voila donc avec la base, **le matériel pour lancer notre infrastructure**. Y'a plus qu'à !

# Installation des serveurs

L'idée de l'installation va être de garder un **maximum de place** pour le cluster ceph. Les serveurs doivent être **jetables** et **remplaçables** facilement, on a donc pas besoin de RAID ou de sécuriser particulièrement le système.

Pour cela on va se baser sur une partition de **100Go** pour le système sur le premier disque dur, le deuxième est laissé entièrement vierge.
Normalement votre hébergeur doit vous laisser la main sur ce formatage pour des serveurs physiques, chez Hetzner ça passe par un [fichier texte](https://wiki.hetzner.de/index.php/Installimage/en) à remplir en mode rescue.
Debian 9 est une bonne base pour notre infrastructure. J'aurai bien voulu en profiter pour le chiffrer, mais on sort du cadre de cet article et de ma motivation ici :-D

## Openvpn
A moins que vous ayez le budget en conséquence pour vous payer un réseau privé sécurisé entre vos serveurs, on va ici se baser sur Openvpn pour monter un réseau chiffré qui servira à la fois pour ceph et pour kubernetes.

L'idée étant de ne pas avoir de point faible dans l'infrastructure, on va contourner la faiblesse client/serveur d'Openvpn en utilisant une technique de **Ring** :
Chaque **client** VPN est également **serveur** VPN, et peut donc se connecter aux autre ou recevoir leurs connexions. De cette manière on passe d'une topologie en étoile à une topologie en cercle, plus robuste !

![Ring VPN](/images/peertube-k8s-ceph/ringvpn.png)

On va commencer par configurer un serveur VPN en étoile classique et ses clients, de la même manière que dans mon article sur [installer openvpn sur debian 9]({filename}/2018/2018-01-04-serveur-openvpn-sous-debian-9-et-systemd.md).

Une fois le serveur VPN fonctionnel et vos 2 clients connectés dessus (les 2 autres serveurs physiques), on va le transformer en structure en cercle.

Pour cela c'est très simple, il suffit de copier le fichier de configuration du serveur **et ses clefs** sur les différents serveurs physiques, exactement comme pour le premier serveur VPN.
Une fois ces serveur démarrés, vous disposez de 3 serveurs VPN identiques.

**Attention**, vous aurez remarqué que le serveur VPN original n'a pas de client, c'est normal car il ne se connectera à personne pour éviter les boucles réseaux. Notez le sens des flèches sur le schéma ci-dessus.

Mais vos clients ne se connectent toujours qu'au premier serveur, et c'est ici qu'arrive la subtilité du cercle.
On va modifier les fichiers de configuration **clients** pour leur dire qu'il y a plusieurs serveurs disponibles :

```
remote XXX.XXX.XXX.XXX 1994
remote YYY.YYY.YYY.YYY 1994
keepalive 30 120
```

Ici on dit à notre client VPN d'essayer de se connecter à XXX en premier. S'il n'est pas disponible, il va essayer de se connecter à YYY.
Au cas où un serveur est indisponible, `keepalive` signifie qu'il va essayer toutes les 30 secondes ses différents `remote`, avec un timeout de 120 secondes.

Ainsi en cas de panne d'un serveur, au bout de 120 secondes max les clients essaieront de se connecter au serveur suivant et reconstruiront le réseau.

Il s'agit ici d'un compromis, car en cas de panne du serveur principal, votre cluster sera dans le noir pendant plusieurs minutes le temps que tout se reconstruise, mais une fois les VPN reconnectés ceph et kubernetes retrouveront leurs petits.
Attendez vous également à des performances moindres qu'un véritable réseau dédié mais on fait avec le budget ;-)

En outre cette technique peut tout à fait être utilisée entre différents hébergeurs ou datacenters pour plus de résilience avec des performances très correctes.

## Ceph

Une fois le serveur installé, on va commencer par installer **ceph**, qui sera ainsi prêt à recevoir kubernetes et ses provisionner (les outils kubernetes qui parlent au système de stockage).

Comme je suis un grand utilisateur de **Ansible**, j'ai décidé d'utiliser le projet *[ceph-ansible](https://github.com/ceph/ceph-ansible) pour déployer ceph.
L'avantage est d'avoir un ensemble de playbook pour gérer le cluster et de pouvoir ainsi rajouter des serveurs de manière très simple. Là encore l'idée est de pouvoir jeter, remplacer et ajouter des serveurs le plus facilement possible.

Ici va se poser la question du type de stockage que vous souhaitez utiliser avec ceph. Les plus connus sont Rados Block Device (RBD) et Ceph FileSystem (cephfs).

Chacun ont des avantages et des inconvénients, mais dans kubernetes la principale différence est que RBD ne peut marcher qu'en ReadWriteOnce, c'est à dire qu'un stockage ne peut être monté que sur une seule node à la fois.
D'expérience ça pose quelques problèmes avec le fonctionnement de k8s, notamment en cas de soucis sur une node (crash, perte de réseau, ...) si elle n'a pas pu annoncer qu'elle relachait le RBD, votre pod qui l'utilise ne pourra jamais redémarrer ailleurs ûisque pour k8s le stockage sera toujours utilisé...

Cephfs de son côté présente l'avantage de permettre du ReadWriteMany et en outre comme tout filesystem on lui définit un quota plutôt quu'ne taille fixe, ce qui est pratique quand on a besoin d'un stockage extensible comme pour Peertube.
J'ai donc choisi **Cephfs** comme système de stockage via ceph.

Cela impose quelques subtilités lors du déploiement, notamment la création de pools ceph dédié à cephfs car il a besoin de stocker ses data et ses metadata.

Pour ce faire, on va ajouter dans les **variables de déploiement** de ceph-ansible les valeurs suivantes :
(on parle bien ici de variables ansible)


```
cephfs: cephfs # nom du filesystem
cephfs_data: cephfs_data # nom du pool de données du cephfs
cephfs_metadata: cephfs_metadata # nom du pool pour les metadata du cephfs
```

Il vous faudra également créer les **pools** correspondants:

```
cephfs_pools:
  - { name: "{{ cephfs_data }}", pgs: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}", pg_num: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}", pgp_num: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}" }
  - { name: "{{ cephfs_metadata }}", pgs: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}", pg_num: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}", pgp_num: "{{ hostvars[groups[mon_group_name][0]]['osd_pool_default_pg_num'] }}" }
```

Pour ce qui est du reste des variables, elles sont communes à un déploiement ceph, je vous renvoie vers la [documentation de ceph-ansible](http://docs.ceph.com/ceph-ansible/master/)

Le nombre de pg à définir notamment va grandement dépendre de la taille de votre cluster. Ceph propose un [outil](https://ceph.com/pgcalc/) pour le calculer selon vos besoins.
A titre indicatif, pour le cluster avec 6 disques de 3To, j'utilise 64 pg par pool.

## Kubernetes
Concernant kubernetes, j'ai décidé d'utiliser l'excellent [kubespray](https://github.com/kubernetes-sigs/kubespray) qui permet de configurer et maintenir un cluster kubernetes avec Ansible.

La configuration par défaut est fonctionnelle, pensez à spécifier votre `bootstrap_os` pour coller à vos serveur (debian dans mon cas).

Concernant les variables à modifier, quelques bonnes pratiques de sécurités consistent à forcer tout le TLS possible, et à désactiver tout ce qui est authentification anonyme, que ce soit sur l'API du cluster ou sur l'API interne des nodes :

```
authorization_modes: ['Node', 'RBAC']
kubelet_authentication_token_webhook: false
kubelet_authorization_mode_webhook: false
```

Pensez également à activer le **provisionneur** cephfs directement, puisque kubespray le supporte par le biais d'un [addons](https://github.com/kubernetes-incubator/external-storage/tree/master/ceph/cephfs)

```
# CephFS provisioner deployment
cephfs_provisioner_enabled: true
cephfs_provisioner_namespace: "cephfs-provisioner"
cephfs_provisioner_cluster: ceph
cephfs_provisioner_monitors: "10.8.0.1:6789,10.8.0.2:6789,10.8.0.3:6789"
cephfs_provisioner_admin_id: admin
cephfs_provisioner_secret: YourSecret
cephfs_provisioner_storage_class: cephfs
cephfs_provisioner_reclaim_policy: Delete
cephfs_provisioner_claim_root: /volumes
cephfs_provisioner_deterministic_names: true
```

**Remplacez** bien sur les IP des monitors avec vos propres IP, celles utilisées dans le réseau OpenVPN ;-)

Le secret ceph se retrouvera une fois le cluster ceph installé, il est généré par ceph lors de la création des pools pour l'utilisateur admin :

```
# ceph auth get client.admin
```

A titre personnel, j'utilise également helm et le cert-manager proposé en tant qu'addons :

```
# Helm deployment
helm_enabled: true
# Cert manager deployment
cert_manager_enabled: true
cert_manager_namespace: "cert-manager"
```

# Ansible et déploiement

Concernant l'inventaire ansible, il reste assez simple dans l'optique où les noeuds kubernetes seront les mêmes que les noeuds ceph.

Pour ma part, il ressemble à ça :

```
[k8s]
node1.local osd_crush_location="{ 'root': 'default', 'rack': 'cephfs_data', 'pod': 'pod1', 'host': 'node1.local' }"
node2.local osd_crush_location="{ 'root': 'default', 'rack': 'cephfs_data', 'pod': 'pod1', 'host': 'node2.local' }"
node3.local osd_crush_location="{ 'root': 'default', 'rack': 'cephfs_data', 'pod': 'pod1', 'host': 'node3.local' }"

[kube-master:children]
k8s

[etcd:children]
k8s

[kube-node:children]
k8s

[k8s-cluster:children]
kube-master
kube-node

[mons:children]
k8s

[osds:children]
k8s

[clients:children]
k8s

[mdss:children]
k8s

[mgrs:children]
k8s
```

Au final vous devriez avoir une arborescence de ce type pour l'inventaire ansible :

```
.
├── ansible.cfg
├── group_vars
│   ├── all
│   │   ├── ceph_pool.yml
│   │   ├── ceph.yml
│   │   ├── k8s-addons.yml
│   │   ├── k8s-cluster.yml
│   │   └── kubespray.yml
│   ├── clients.yml
│   ├── mons.yml
│   └── osds.yml
├── hosts
├── host_vars
│   ├── node1.local
│   ├── node2.local
│   └── node3.local
```

Il ne vous reste alors plus qu'à déployer !

Comme kubernetes a besoin de ceph pour fonctionner, je vous conseille de commencer par ce dernier.
Attention à utiliser **ansible 2.5** pour ces déploiements (au 30/12/18, ceph-ansible ne supporte pas plus)

```
[ceph-ansible] ± cp site.yml.sample site.yml
[ceph-ansible] ± ansible-playbook -i ../your_inventory/hosts site.yml

[ceph-ansible] ± cd ~/kubespray
[kubespray] master ± ansible-playbook -i ../your_inventory/hosts cluster.yml
```

Les déploiements peuvent prendre un certain temps selon votre bande passante et le nombre de noeuds, parce qu'il y a quand même pas mal de chose à faire.

Si vous rencontrez des erreurs, les documentations sont en général suffisantes pour les corriger facilement, et les deux playbook sont **idempotents** (notamment kubespray) donc pas trop de problème pour les repasser en cas de crash.

Si vous rencontrez de gros soucis durant le déploiement de ceph, n'hésitez pas à purger le cluster pour recommencer, après tout vous n'avez pas de données dessus pour l'instant !

**Note :** il s'agit ici d'utiliser ansible pour aider au déploiement. Vous pouvez tout à fait utiliser un autre outil ou déployer à la main si vous le préférez. **Ne mettez pas en production un outil que vous ne maitrisez pas !**

Au final, vous disposez d'un cluster **kubernetes** fonctionnel avec une base ceph pour le stockage persistent ! Il va donc être temps de s'amuser avec ;-)

# Sécurité

D'un point de vue sécurité, il est très important que votre cluster ne soit accessible que sur les **IP du VPN**.
Bloquez les accès à l'API kubernetes sur le port **6443** car elle a tendance à écouter un peu n'importe où, utilisez exclusivement des certificats pour les utilisateurs !

Kubespray de base est plutôt sécurisé, ce qui est assez plaisant. Notamment il déploie des certificats pour sécuriser toutes les connexions (etc, api, kubelet, ...) mais cela n'empêche pas de limiter les accès pour éviter les angles d'attaques.

Concernant ceph, si vous lui avez fourni les bonnes IP VPN pour son déploiement, il ne devra jamais se retrouver exposé à l'extérieur.

Selon ce que vous souhaitez héberger dans votre cluster, les **seuls ports** à ouvrir devraient être ceux des services hébergés.

Par exemple dans mon cas pour pair2jeux, seuls les ports 80 et 443 sont exposés pour joindre Peertube et les outils web associés.

# Conclusion

Les clusters (ceph et kubernetes) proposés ici sont maintenables via les projets kubespray et ceph-ansible, que ce soit pour ajouter des nodes, en détruire ou pour mise à jour de l'infrastructure.

Il vous faudra choisir comment configurer vos accès aux services (j'utilise pour ma part [haproxy-ingress](https://github.com/jcmoraisjr/haproxy-ingress/).

Cette infrastructure me permet de faire tourner l'instance peertube de pair2jeux avec une assez confortable marge de stockage et d'y associer d'autres outils (wiki, ...) de manière fun.

Plusieurs améliorations peuvent être mises en place sur cette infrastructure, comme par exemple

 - Un système de log centralisé (très utile pour les pods kubernetes)
 - Un monitoring des services kubernetes et des pods
 - Des sauvegardes des données ceph et de l'etcd
 - Un système d'IP virtuelle pour utiliser des entrées DNS qui tapent toujours dans le cluster fonctionnel en cas de panne
 - Intégrer l'espace disque restant au cluster ceph
 - Séparer les master kubernetes des nodes (quand le budget sera plus étendu)
 - Trouver des serveurs avec SSD pour faire tourner le système

J'essaierai de traiter certains de ces points dans de futurs articles, en attendant j'espère que celui vous aura plu !

### Remerciements

Pour leur relecture :

 - noskillbrain ([https://noskillbrain.fr/](https://noskillbrain.fr/))
 - Dryusdan ([https://www.drycat.fr/](https://www.drycat.fr/))
 - Nailyk