Blog de dada

DevOps, bidouilleur et routard plein de logiciels libres

kubernetes

Crontab ? Non, Cronjob !

Rédigé par dada / 15 février 2019 / Aucun commentaire



On sait tous, pour peu que notre métier consiste à passer trop d'heure devant des écrans branchés en SSH à des serveurs, qu'une action répétitive doit être gérée par la Crontab. C'est normal.
Kubernetes s'est amusé à reprendre ce principe pour y glisser du Docker, parce que voilà. Pas vraiment étonnant. Regardons ensemble ce qu'est un CronJob, l'équivalent Crontab de l'orchestrateur.

CronJob

L'idée du CronJob, c'est d'aller effectuer le travail de la Crontab alors qu'il n'est pas possible de savoir combien de conteneurs font tourner notre application. Eh oui, avec un seul conteneur, il est possible de prendre un peu de temps pour lui glisser le paquet qui va bien et ajouter la ligne de configuration qui fait le boulot, mais avec Kubernetes, c'est une autre affaire. Imaginez que vous fassiez ça avec plusieurs conteneurs : vous avez beau vous amuser à calculer le temps d'exécution du processus et ainsi décaler son lancement à la louche, vous allez vite vous retrouver avec des nœuds qui load et des données corrompues. Bref.

Matomo et son CronJob

En exemple, voici ce que je fais avec mon Matomo. Rappel rapide: Matomo est un outil d'analyse de fréquentation d'un site web bien plus propre que ses concurrents et qui, pour un logiciel libre, ne néglige pas les beaux graphiques.

La configuration

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: matomo-archive
spec:
  schedule: "*/30 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: matomo-cron
            image: matomo:3.7
            volumeMounts:
            - name: pv-matomo
              mountPath: /var/www/html
            args:
            - /bin/bash
            - -c
            - /usr/local/bin/php /var/www/html/console core:archive --url=https://url.de.mon.matomo/
          restartPolicy: OnFailure
          volumes:
          - name: pv-matomo
            flexVolume:
              driver: ceph.rook.io/rook
              fsType: ceph
              options:
                fsName: myfs
                clusterNameSpace: rook-ceph
                path: /matomo
Si vous êtes perdus, souvenez-vous que je me sers d'un cluster Ceph géré par Rook. Grace à ça, je peux monter le volume contenant les sources nécessaire à l'exécution du cron, parce qu'il faut bien lancer du PHP à un moment ou un autre.

Explications

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: matomo-archive
spec:
  schedule: "*/30 * * * *"
Ici, c'est la ligne schedule qui est importante. Les habitués de la Crontab comprendront qu'elle permet de définir la fréquence d'exécution. Ici, toutes les 30 min.
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: matomo-cron
            image: matomo:3.7
À l'heure où j'écris ces lignes, la version 3.8 de Matomo est disponible mais pas son conteneur, d'où la version 3.7 précisée dans la configuration du conteneur. Gros rappel : latest n'est pas une version !
            args:
            - /bin/bash
            - -c
            - /usr/local/bin/php /var/www/html/console core:archive --url=https://url.de.mon.matomo/
La partie args permet de spécifier l'action du CronJob : ici, on va lancer un script PHP développé par Matomo avec l'url de son instance en paramètre. Tout simplement.
          volumes:
          - name: pv-matomo
            flexVolume:
              driver: ceph.rook.io/rook
              fsType: ceph
              options:
                fsName: myfs
                clusterNameSpace: rook-ceph
                path: /matomo
La partie volumes est toute basique : c'est là que je configure l'accès aux sources du conteneur qui va lancer la tâche.

On applique tout ça :
kubectl apply -f cronjob.yaml
Et on attend 30 min que le CronJon prenne la peine de démarrer. N'hésitez vraiment pas à baisser la fréquence d'exécution si vous n'avez pas que ça à faire. 5min, c'est très bien. 1min ? Laissez tomber, par contre.

Vérification

Pour vérifier que vous avez bien ce que vous avez demandé :
dada@k8smaster1:~/matomo$ k get cj
NAME             SCHEDULE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
matomo-archive   */30 * * * *   False     0        21m             28d
On retrouve bien le nom du CronJob, sa configuration et quelques infos comme la dernière exécution (21 minutes) et son age (28 jours).

Avec le temps, au bout de 1h30 si vous avez copié/collé ma configuration, vous allez remarquer que le cluster garde en mémoire les 3 dernières tentatives :
dada@k8smaster1:~/matomo$ k get pods | grep archive
matomo-archive-1548606600-4cwsl         0/1     Completed   0          84m
matomo-archive-1548608400-7lxf8         0/1     Completed   0          54m
matomo-archive-1548610200-cwh6t         0/1     Completed   0          24m
Ça vous permet d'avoir un accès aux logs rapidement. Chouette !

Vous pouvez maintenant aller jouer avec !

De retour du FOSDEM 2019

Rédigé par dada / 07 février 2019 / Aucun commentaire


Qu'on aime ou pas cet événement, la Réunion Européenne des Développeurs de Logiciels Libres et Open Source est devenue mon week-end préféré de l'année. Ou presque.
C'est toujours avec un plaisir fou que je me retrouve entouré des gens avec qui je ne peux discuter qu'à travers un écran le reste de l'année : du suisse, de l'allemand, du français, du finlandais et, nouveauté 2019, un américain ! Bref, des copains, trop d'anglais, des bières belges et des discussions jusqu'à pas d'heure.

Cette année et contrairement aux autres fois, j'ai réussi à assister à des conférences et à boire de la bière avec modération ! Je vous assure que ce n'est pas une mince affaire quand on connaît le talent de nos amis belges en matière de brasserie. Le vin est à la France ce que la bière est à la Belgique !

Voici un aperçu de ce que j'ai vu. Tout est en anglais et je ne crois pas qu'il existe un moyen de mettre des sous-titres. Si la langue d'Ed Sheeran n'est pas votre fort, passez votre chemin.

Ceph storage with Rook : Running Ceph on Kubernetes


Je connaissais déjà la bête et vous êtes en plein dedans si vous lisez cet article. Je pensais pouvoir découvrir deux ou trois trucs mais la démonstration qui devait être faite à la fin de la présentation n'a pas pu se faire : souci d'écran.

Loki - Prometheus for logs


Sur celle-là, j'ai carrément jubilé ! Ce truc a l'air vraiment super sympa. J'ai déjà essayé de le faire tourner dans mon cluster mais ce n'est pas vraiment stable. En plus clair, ça fait tomber les nodes un par un à cause d'une consommation de mémoire vive ahurissante.

ActivityPub Panel

 

Assez inévitable quand on est à fond dans le Fédiverse : la table ronde autour d'AP. C’était intéressant et je vous encourage à la regarder même si Chris Webber et son accent américain fatiguent rapidement les oreilles.

Matrix in the French State

 

Assister à une conférence qui parle à la fois de Matrix et de l'État Français, je ne pouvais pas rater ça ! C'était fichtrement intéressant, même si le bonhomme s'est amusé à comparer l'ANSSI à la NSA...

The Cloud Is Just Another Sun

 

Je ne savais pas trop à quoi m'attendre en allant voir cette conférence. Faut dire que j'avais pas totalement lu le descriptif. On y apprend qu'il faut se méfier du Cloud. Je vous invite vivement à la regarder, le mec est clair, et à vous forger une opinion sur le sujet.

Pour le lolz

On est allé, avec les gars de Feneas, voir la conférence autour du réseau social basé sur la blockchain : Hey. Pas grand chose à en dire. C'était technique et très concentré autour de la chaîne de blocs. Je n'ai pas compris grand chose.

Bref

Des litres de bières, des rencontres, des sujets qui font du bien au cerveau et surtout, mais surtout, des gens avec qui on n'arrive jamais à capter : c'est ça le FOSDEM, mon FOSDEM, que j'aime tant !

Merci aux organisateurs et à l'année prochaine !

Nextcloud, PHP-FPM, Nginx et Kubernetes

Rédigé par dada / 14 janvier 2019 / 7 commentaires




Ma première installation de Nextcloud dans Kubernetes était basée sur l'image Docker contenant Apache2. Aucun souci notable au niveau de la synchro des agendas, des fichiers ou encore des contacts. Par contre, la génération des miniatures des photos s'est révélée être un drame : Apache s'emballait et entraînait le nœud sur lequel il tournait avec lui, dans la tombe. Il me fallait une solution, j'ai donc décidé de changer de conteneur et de prendre celui basé sur PHP-FPM.

Un pod avec deux conteneurs

On entend souvent la rumeur racontant qu'un pod ne contient qu'un conteneur. C'est souvent vrai, mais c'est aussi faux. Dans l'exemple qui va suivre, le pod gérant Nextcloud contiendra le conteneur officiel de Nextcloud et un conteneur Nginx.

Contexte

Pour suivre, sachez que mon cluster, celui grâce auquel vous lisez ces quelques lignes, gère son système de fichier avec Rook, dont j'ai déjà parlé ici. Mes nœuds sont chez Hetzner, ce sont des CX21, du cloud public donc, et mes services sont exposés en NodePort derrière un Nginx configuré en LoadBalancer. Maintenant que vous savez ça, on peut y aller.

Le Deployment

On va commencer par balancer le Yaml qui marche :
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextcloud-deployment
spec:
  selector:
    matchLabels:
      app: nextcloud
  replicas: 1
  template:
    metadata:
      labels:
        app: nextcloud
    spec:
      containers:
      - name: nginx
        image: nginx:1.15
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        - name: pv-nextcloud
          mountPath: /var/www/html
        lifecycle:
          postStart:
            exec:
             command: ["bin/sh", "-c", "mkdir -p /var/www/html"]
      - name: nextcloud
        image: nextcloud:14.0-fpm
        ports:
        - containerPort: 9000
        volumeMounts:
        - name: pv-nextcloud
          mountPath: /var/www/html
        resources:
          limits:
            cpu: "1"
      volumes:
      - name : nginx-config
        configMap:
           name: nginx-config
      - name: pv-nextcloud
        flexVolume:
          driver: ceph.rook.io/rook
          fsType: ceph
          options:
            fsName: myfs
            clusterNamespace: rook-ceph
            path: /nextcloud2

Il n'y a pas le Service associé pour la simple et bonne raison que chacun fait comme il le veut. Si vous êtes chez DigitalOcean, OVH ou chez un des GAFAM qui propose du k8s, vous aurez un LoadBalancer qui va bien. Si vous êtes comme moi, vous êtes réduit à faire du NodePort.

Ce qu'il faut comprendre

Vous remarquerez qu'il y a deux conteneurs : Nginx et Nextcloud-FPM. Nginx écoute sur le port 80 et va router le trafic à travers vers le port 9000 du conteneur de Nextcloud.

Nginx

      - name: nginx
        image: nginx:1.15
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        - name: pv-nextcloud
          mountPath: /var/www/html
        lifecycle:
          postStart:
            exec:
             command: ["bin/sh", "-c", "mkdir -p /var/www/html"]
On va faire gober à Nginx deux points de montage : sa configuration et les sources de Nextcloud. Sans les sources de l'application, Nginx ne pourra pas avoir accès aux fichiers PHP, et ne servira donc à rien. On va donc prendre le point de montage originalement dédié à Nextcloud pour le monter une deuxième fois dans un deuxième conteneur, celui de Nginx.

Lifecycle

Remarquez la présence de la section lifecycle, elle permet d’exécuter ce que vous voulez au démarrage du conteneur. Quand j'apprenais à me servir de ce couple, je ne comprenais pas pourquoi Nginx ne voulait pas correctement fonctionner. J'ai passé du temps à comprendre que le conteneur Nginx et le conteneur Nextcloud n'avaient pas le même docRoot :
  • Nginx : /srv/html
  • Nextcloud : /var/www/html
Comprenez que les requêtes Nginx allaient chercher des fichiers dans /srv/html/blabla.php quand Nextcloud annonçait la présence de ses sources dans /var/www/html/blabla.php. Le bordel.

C'est là que je n'ai trouvé pas idiot l'idée de créer le chemin manquant au démarrage du pod avec un postStart. Du coup, j'avais Nginx et Nextcloud au diapason. Il est sans doute possible de configurer Nginx pour surcharger son docRoot, mais c'était l'occasion de jouer avec des commandes en amont de la création d'un conteneur.

Les deux points de montage

On a donc un point de montage pour les sources de Nextcloud :
        - name: pv-nextcloud
          mountPath: /var/www/html
Et un point de montage pour la configuration de Nginx :
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf

Là aussi, j'ai perdu un peu de temps avant de comprendre qu'il fallait balancer toute la configuration de Nginx et pas seulement ce que j'ai l'habitude de mettre dans les sites-enabled. C'est du moins à faire quand on écrase le nginx.conf du pod. En y réfléchissant, c'est sans doute plus simple de modifier le point montage pour n'ajouter qu'un fichier dans le fameux sites-enabled.

Pour gérer la configuration de Nginx, je passe par une ConfigMap :
kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-config
data:
  nginx.conf: |
    worker_processes  1;

    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;

    events {
        worker_connections  1024;
    }

    http {
        include       /etc/nginx/mime.types;
        default_type  application/octet-stream;

        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';

        access_log  /var/log/nginx/access.log  main;

        sendfile        on;
        #tcp_nopush     on;

        keepalive_timeout  65;

        #gzip  on;

        server {
            listen 80;

            add_header X-Content-Type-Options nosniff;
            add_header X-XSS-Protection "1; mode=block";
            add_header X-Robots-Tag none;
            add_header X-Download-Options noopen;
            add_header X-Permitted-Cross-Domain-Policies none;
            add_header Referrer-Policy no-referrer;

            root /var/www/html;

            location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
            }

            location = /.well-known/carddav {
                return 301 $scheme://$host/remote.php/dav;
            }
            location = /.well-known/caldav {
                return 301 $scheme://$host/remote.php/dav;
            }

            # set max upload size
            client_max_body_size 10G;
            fastcgi_buffers 64 4K;

            # Enable gzip but do not remove ETag headers
            gzip on;
            gzip_vary on;
            gzip_comp_level 4;
            gzip_min_length 256;
            gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
            gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

            location / {
                rewrite ^ /index.php$request_uri;
            }

            location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
                deny all;
            }
            location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
                deny all;
            }

            location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
                fastcgi_split_path_info ^(.+\.php)(/.*)$;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info;
                # fastcgi_param HTTPS on;
                #Avoid sending the security headers twice
                fastcgi_param modHeadersAvailable true;
                fastcgi_param front_controller_active true;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_intercept_errors on;
                fastcgi_request_buffering off;
            }

            location ~ ^/(?:updater|ocs-provider)(?:$|/) {
                try_files $uri/ =404;
                index index.php;
            }

            # Adding the cache control header for js and css files
            # Make sure it is BELOW the PHP block
            location ~ \.(?:css|js|woff|svg|gif)$ {
                try_files $uri /index.php$request_uri;
                add_header Cache-Control "public, max-age=15778463";
                add_header X-Content-Type-Options nosniff;
                add_header X-XSS-Protection "1; mode=block";
                add_header X-Robots-Tag none;
                add_header X-Download-Options noopen;
                add_header X-Permitted-Cross-Domain-Policies none;
                add_header Referrer-Policy no-referrer;

                # Optional: Don't log access to assets
                access_log off;
            }

            location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
                try_files $uri /index.php$request_uri;
                # Optional: Don't log access to other assets
                access_log off;
            }
        }

    }
Eh oui, il y a tout dedans. Ça déforme l'affichage de ce billet, m'enfin. C'est une configuration Nginx classique.

On peut quand même s'arrêter sur la configuration du fastcgi_pass : il tape sur le 127.0.0.1 et le port 9000 du conteneur Nextcloud. Je n'ai pas encore gratté pour comprendre le pourquoi du comment mais je suppose que les deux conteneurs tournant dans le réseau du pod, ils se comportent comme deux services dans une seule et même machine. À confirmer.

On apply tout ça

Attention ! Avant de balancer le Deployment, balancez le yaml de la ConfigMap. Sans ça, Nginx ne chargera pas votre configuration !
dada@k8smaster1:~$ kubectl apply -f configmap.yaml
dada@k8smaster1:~$ kubectl apply -f nextcloud.yaml
Si tout se passe bien, vous devriez pouvoir voir ça :
dada@k8smaster1:~$ kubectl get pods
nextcloud-deployment-d6cbb8446-87ckf   2/2     Running   0          15h
Remarquez que Kubernetes vous montre bien qu'il y a deux conteneurs dans ce pod : 2/2.

Pour aller plus loin

Je ne parle pas des vérifications de l'état des conteneurs. Il faudrait placer des sondes liveness et readness pour parfaitement vérifier l'état des conteneurs. Sans ça, si l'un des services tombe, Kubernetes ne sera pas forcément en mesure de le détecter et de relancer le pod.
Il est aussi possible, pour respecter le concept de micro-service, de ne pas concaténer deux conteneurs dans un seul pod mais de faire un pod par conteneur et des services associés. Ça demande plus de travail pour un résultat qui, dans mon cas, n'apporte pas grand chose.

Astuces du dimanche #6

Rédigé par dada / 06 janvier 2019 / Aucun commentaire


Ça faisait longtemps que je n'avais pas fait de ADD. En voici une très orienté Kubernetes.

Grafana

Vous avez installé le Prometheus Operator et vous ne savez pas où est passé le traditionnel couple utilisateur/mot de passe admin/admin ? L'info est ici : l'utilisateur est bien admin, mais le mot de passe est prom-operator.

Rook

Impossible de mettre la main sur le Dashboard ? La configuration a changé avec la version 0.9 : tout est ici.
Votre cluster hurle à coup de HEALTH_ERR ? Toujours à cause de la 0.9, deux modules sont en erreur : l'issue est ici. Rien de grave, le cluster tourne bien malgré les alertes. Les manipulations pour corriger le tir sont décrites par là.

Version de Docker

Les versions de docker se suivent et ne sont pas toujours compatibles avec ce que demande Kubernetes, pour installer une version précise :
export VERSION=18.03 && curl -sSL get.docker.com | sh
Prenez le temps de bien supprimer/purger l'ancienne version de docker avant de lancer la commande et d'installer Git. Notez aussi que ça vous empêchera d'utiliser le bon vieux couple "apt update && apt upgrade".

Init d'un Cluster k8s

Si vous installez un master sur un serveur n'ayant qu'un seul CPU, ignorez l'erreur avec :
--ignore-preflight-errors=NumCPU
Si vous voulez forcer l'API à utiliser un réseau spécifique, genre un réseau privé basé sur, disons, WireGuard :
--apiserver-advertise-address="10.0.42.1"
Où 10.0.42.1 est l'IP de l'interface VPN de votre Master.

Méfiez-vous des Resources

Utiliser Kubernetes sur des petits serveurs comme ceux que j'utilise chez Hetzner peut vous pousser à mettre en place des limitations de ressources sur vos conteneurs. Dans mon cas très particulier, où je suis quasiment le seul utilisateur, il est plus efficace de ne pas limiter les conteneurs, quitte à les voir éclater le CPU pendant quelques secondes. Avec des limitations, j'ai réussi à monter à plus de 200 de load pour 2 CPU. Voilà.

Kubernetes en multi-master sur du baremetal avec HAProxy

Rédigé par dada / 20 décembre 2018 / 12 commentaires



Quand on joue avec Kubernetes, on se rend compte que c'est aussi puissant que fragile. L'orchestration, ça ne s'improvise pas. Il faut vraiment passer des heures à se casser les dents sur des clusters de tests qui finiront toujours par s'écrouler. Que ce soit à cause d'un souci côté hébergeur, entraînant une indisponibilité de votre master ou une configuration laxiste permettant à des pods de consommer plus de ressources que ce qu'il y a de disponible, en massacrant chaque node disponible, méticuleusement, les uns après les autres, jusqu'à vous laisser sans rien.
Je ne parlerai pas ici du meilleur moyen pour contrôler la consommation des pods. On va s'attaquer au premier souci : votre unique master configuré en SPoF.

Le multi-master, ton ami

Pour éviter de voir un cluster HS à cause de l'absence de son maître, on va le configurer avec un quorum de... 3 masters. Tout simplement.

Pourquoi HAProxy ?

Mon environnement de test est basé sur des serveurs dans le cloud de Hetzner. J'ai essayé d'attendre aussi longtemps que possible des infrastructures abordables supportant k8s loin des AWS, GCP ou encore Azure, en vain. Il y a bien OVH et DigitalOcean mais le côté "abordable" n'y est pas. Le premier ouvre son infrastructure à partir de 22€ le serveur, et l'autre 20$ (node unique + LB). On est loin des 6€ par machine chez Hetzner.

L'idée

Comme l'indique la documentation officielle, HAProxy va faire ce qu'il sait faire de mieux : rediriger les requêtes. Comment peut-on faire croire à 3 serveurs séparés qu'ils bossent ensemble ? En installant un HAproxy sur chacun d'entre eux, configuré pour loadbalancer les requêtes qui seront balancées sur le 127.0.0.1 et le port 5443 (choisi arbitrairement).

En image, ça donne ça :


Configurés comme ça, nos masters vont s'amuser entre-eux sans aucun souci.

L'installation

Je passe sur la configuration de Kubernetes, j'en parle déjà ici. Notez quand même que vous pouvez suivre ce que je raconte jusqu'à l'init du cluster. Pas plus. Pourquoi ? Parce que sur le premier de vos masters, nous allons ajouter un fichier de configuration :
root@k8smaster1:~# cat kubeadm-config.yaml 
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
apiServer:
  certSANs:
  - "127.0.0.1"
controlPlaneEndpoint: "127.0.0.1:5443"
networking:
   podSubnet: "10.244.0.0/16"
Ici, on voit bien que j'ai configuré mon k8s pour taper sur le localhost de la machine à travers le port 5443. C'est là que HAproxy prend la relève.
Je me suis permis d'ajouter la configuration nécessaire à Flannel, mon CNI, qui nécessite en CIDR particulier pour fonctionner correctement. C'est la partie networking.

On peut enchaîner sur la configuration de HAProxy :
global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private

    ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
    ssl-default-bind-options no-sslv3

defaults
    log    global
    mode    tcp
    option    tcplog
    option    dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

frontend api-front
    bind 127.0.0.1:5443
    mode tcp
    option tcplog
    use_backend api-backend

backend api-backend
    mode tcp
    option tcplog
    option tcp-check
    balance roundrobin
    server master1  10.0.42.1:6443 check
    server master2  10.0.42.2:6443 check
    server master3  10.0.42.3:6443 check
La partie global a l'exacte configuration par défaut. J'ai touché à rien.

Dans defaults, j'ai changé le mode de proxy pour le passer de http à tcp.

Les parties frontend et backend regroupent les configurations qui vont bien pour permettre à HAProxy de faire ce que je lui demande. Vous devriez pouvoir recopier tout ça en prenant seulement le soin de changer les IP des masters.

Pour comprendre plus clairement la configuration de HAproxy, je vous redirige vers cet excellent billet de Victor HÉRY.

Une fois configurés, n'oubliez pas de redémarrer les HAProxy sur tous les serveurs et de vérifier qu'ils écoutent bien sur le bon port :
root@k8smaster1:~# nc -v localhost 5443
localhost [127.0.0.1] 5443 (?) open
Astuce : pensez à installer hatop. C'est vraiment super ! Pour le lancer :
hatop -s /var/run/haproxy/admin.sock
Pour le moment, tout sera down, mais une fois le premier master en état, il apparaîtra UP.

On va maintenant initialiser le premier master :
kubeadm init --config=kubeadm-config.yaml
Une fois terminé, vous allez récupérer les informations classiques : le join qui va bien.
kubeadm join 127.0.0.1:5443 --token a1o01x.tokenblabla --discovery-token-ca-cert-hash sha256:blablablablalblawhateverlablablameans --experimental-control-plane
Notez que l'IP de l'API est bien 127.0.0.1 avec le port spécifié à HAProxy. C'est tout bon ! Faites un tour dans hatop et remarquez que le backend du master1 est enfin marqué comme étant UP.

Un get nodes vous confirmera l'arrivée du master1 et un get pods vous montrera les conteneurs de base.

Installons maintenant Flannel, toujours aussi simplement :
kubectl apply -f https://github.com/coreos/flannel/raw/master/Documentation/kube-flannel.yml
Et on aura un premier master en état de fonctionner !

Pour configurer les deux autres masters, il va falloir jouer du SCP. La doc officielle fournit 2 scripts bash que vous pouvez utiliser. Je ne m'étends pas sur le sujet ici et j’enchaîne sur la configuration des autres masters. Pensez quand même à bien configurer SSH et votre user de base.

Une fois que tout est copié, vous n'avez qu'à lancer la commande join sur les masters restant, un par un, et voir le résultat :
dada@k8smaster1:~$ k get nodes
NAME         STATUS   ROLES    AGE   VERSION
k8smaster1   Ready    master   12h   v1.13.1
k8smaster2   Ready    master   11h   v1.13.1
k8smaster3   Ready    master   11h   v1.13.1
Ou encore :
dada@k8smaster1:~$ k get pods --all-namespaces
NAMESPACE     NAME                                 READY   STATUS    RESTARTS   AGE
kube-system   coredns-86c58d9df4-cx4b7             1/1     Running   0          12h
kube-system   coredns-86c58d9df4-xf8kb             1/1     Running   0          12h
kube-system   etcd-k8smaster1                      1/1     Running   0          12h
kube-system   etcd-k8smaster2                      1/1     Running   0          11h
kube-system   etcd-k8smaster3                      1/1     Running   0          11h
kube-system   kube-apiserver-k8smaster1            1/1     Running   0          12h
kube-system   kube-apiserver-k8smaster2            1/1     Running   0          11h
kube-system   kube-apiserver-k8smaster3            1/1     Running   0          11h
kube-system   kube-controller-manager-k8smaster1   1/1     Running   1          12h
kube-system   kube-controller-manager-k8smaster2   1/1     Running   0          11h
kube-system   kube-controller-manager-k8smaster3   1/1     Running   0          11h
kube-system   kube-flannel-ds-amd64-55p4t          1/1     Running   1          11h
kube-system   kube-flannel-ds-amd64-g7btx          1/1     Running   0          12h
kube-system   kube-flannel-ds-amd64-knjk4          1/1     Running   2          11h
kube-system   kube-proxy-899l8                     1/1     Running   0          12h
kube-system   kube-proxy-djj9x                     1/1     Running   0          11h
kube-system   kube-proxy-tm289                     1/1     Running   0          11h
kube-system   kube-scheduler-k8smaster1            1/1     Running   1          12h
kube-system   kube-scheduler-k8smaster2            1/1     Running   0          11h
kube-system   kube-scheduler-k8smaster3            1/1     Running   0          11h
Un dernier tour sur hatop pour admirer la présence de tous vos backends enfin marqué UP.

Vous avez maintenant un cluster k8s en HA ! La suite serait peut-être de vous raconter comment sortir ETCd du cluster pour le mettre en place dans un cluster séparé dédié, mais non. Ce billet est déjà bien assez long.

Si tout ne se passe pas comme prévu, pensez à regarder la documentation officielle. Mon billet n'est qu'un complément qui peut contenir des coquilles. Pensez aussi à bien vérifier la configuration de votre firewall, ou encore de HAProxy, ou encore de Docker, ou encore votre VPN parce qu'on ne laisse pas tout traîner sur le réseau. Bref.