Blog de dada

DevOps, bidouilleur et routard plein de logiciels libres

DevOps

Loki : jouer avec ses logs dans Grafana

Rédigé par dada / 18 juin 2019 / 3 commentaires



Le 3 juin dernier est sortie la première version beta de Loki : la v0.1.0. J'attendais cette première version depuis un bon moment ! Depuis le FOSDEM, pour être précis. Grand fan de la stack Prometheus / Grafana, je bavais d'impatience de pouvoir mettre les mains dans un système d'agrégation de logs directement dans Grafana. On y est alors regardons comment ça se passe !

Loki

Loki se comporte comme Prometheus : c'est un logiciel que vous allez installer sur votre machine qui sert pour le monitoring de votre infrastructure et le laisser vivre sa vie. Comme son mentor, ou presque, il va falloir lui associer des exporters pour le gaver de données : Promtail.

Installation

Pour le moment, je me sers de Loki dans un conteneur docker, parce que c'est marrant et parce que voilà. Pour le faire tourner comme j'en ai envie, j'ai besoin de deux fichiers :

- le Dockerfile
FROM grafana/loki

COPY local-config.yaml /etc/loki/local-config.yaml

CMD ["/bin/loki", "-config.file=/etc/loki/local-config.yaml"]
- Les conf dans local-config.yaml :
auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  chunk_idle_period: 15m

schema_config:
  configs:
  - from: 2018-04-15
    store: boltdb
    object_store: filesystem
    schema: v9
    index:
      prefix: index_
      period: 168h

storage_config:
  boltdb:
    directory: /tmp/loki/index

  filesystem:
    directory: /tmp/loki/chunks

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

chunk_store_config:
  max_look_back_period: 0

table_manager:
  chunk_tables_provisioning:
    inactive_read_throughput: 0
    inactive_write_throughput: 0
    provisioned_read_throughput: 0
    provisioned_write_throughput: 0
  index_tables_provisioning:
    inactive_read_throughput: 0
    inactive_write_throughput: 0
    provisioned_read_throughput: 0
    provisioned_write_throughput: 0
  retention_deletes_enabled: false
  retention_period: 0

Il n'y a pas grand-chose à raconter sur ces deux fichiers. J'ai volontairement réduit le Dockerfile au strict minimum puisque je ne m'en sers que pour copier la configuration de Loki directement dans le conteneur, histoire de réduire la liste des paramètres à son lancement.

Exécution

Pour lancer Loki, on va taper ça dans son terminal :
docker run -d -p 3100:3100 loki
Si tout s'est bien passé, vous devriez avoir un conteneur qui vous répond des politesses :
root@monito:~# curl 127.0.0.1:3100
404 page not found
C'est normal. Par contre, si vous avez autre chose, c'est pas normal.

Maintenant que Loki tourne comme un grand, il faut bien comprendre qu'il ne sert à rien sans son comparse Promtail.

Promtail

Installation

Pour installer Promtail, on va faire exactement comme avec Loki : un conteneur simple avec un peu plus de paramètres au lancement.

- Le Dockerfile :
FROM grafana/promtail

COPY docker-config.yaml /etc/promtail/docker-config.yaml

ENTRYPOINT ["/usr/bin/promtail", "-config.file=/etc/promtail/docker-config.yaml"]
- La conf dans docker-config.yaml :
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://IP_SRV_LOKI:3100/api/prom/push

scrape_configs:
- job_name: system
  static_configs:
  - targets:
      - localhost
    labels:
      job: varlogs
      __path__: /var/log/*log
Si vous avez le coup d’œil, vous devriez avoir remarqué que cette configuration ressemble furieusement à celle de Prometheus : surtout la partie scrape_configs. On y retrouve la conf des jobs, les targets, labels, etc.
Remarquez qu'ici, vous devez remplacer IP_SRV_LOKI par l'IP du serveur sur lequel vous avez effectivement lancé Loki.
Ici, Promtail va aller gratter tous les fichiers locaux se terminant par log dans le répertoire /var/log/ le tout en les classant dans le job system avec le label varlogs.
Locaux, vous dites ? Mais nous sommes dans un conteneur : par quelle magie est-ce possible ? En jonglant avec les volumes !

Exécution

On build le conteneur :
docker build -t promtail .
Dans ce cas simple, on peut lancer Promtail comme ceci :
docker run -d -p 9095:9095  -v /var/log:/var/log promtail
Voyez qu'on monte les logs locaux de la machine dans le conteneur, les rendant accessibles à Promtail. N'hésitez pas à jouer de exec -it pour vérifier qu'ils sont bien là.

On se retrouve avec un Loki capable d'être gavé et d'un Promtail qui, pour le coup, gave Loki. Chouette.

Grafana

Maintenant que les bases sont en place, allons dire à Grafana de nous afficher tout ça. Si, comme moi, Grafana tourne sur la même machine que Loki, ça va être facile. Il faut ajouter un nouveau Data Source local : http://localhost:3100.

Une fois que c'est fait, foncez jouer dans la rubrique Explore et faites votre sélection ! Bon, pour le moment, vous n'avez que les logs system d'une seule machine, mais c'est déjà ça. Si je vous montre le graphique des logs Nginx de ma machine principale, vous avez ce genre de chose :


Si vous affichez cette capture d'écran en grand, vous devriez bien remarquer le manque de sérieux dans quelques-unes de mes configurations (rien de grave, rassurez-vous). On y voit du gris, du vert et du rouge.

Non, vous n'aurez pas les logs à proprement parler mais faites moi confiance, ils s'entassent en dessous.

L'affichage des logs est manipulable avec le Log Stream Selector. Ce machin permet de filtrer l'affichage simplement. Dans mon exemple, remarquez que je sélectionne le job nginxlogs et le hostname de ma machine.
Vous pouvez aussi filtrer le contenu même des logs, le texte en gros, mais je vais vous laisser chercher par vous-même.

Si vous avez vraiment le coup d’œil, dans la configuration de Promtail que je vous propose juste au dessus, il n'y a pas de hostname, ni de log Nginx d'ailleurs.

Pour que ça marche chez vous, vous devez déjà monter le répertoire de log de Nginx dans le conteneur et utiliser cette configuration :
- job_name: nginx
  static_configs:
  - targets:
      - localhost
    labels:
      job: nginxlogs
      __path__: /var/log/nginx/*log
      hostname: b2d
Vous pouvez l'ajouter à la suite de ce que je propose déjà, ça vous fera déjà plus de logs à parcourir !

Bref. Afficher des logs dans Grafana, c'est chouette, mais Loki ne permet heureusement pas que ça sinon il n'y aurait aucun intérêt à parler d'un truc pas encore terminé.

Loki et Prometheus

Avoir un Loki en parallèle d'un Prometheus, ça permet de coupler la puissance des exporters Prometheus avec les logs pompés par Loki. Et ça, c'est beau ! Comment ? En cliquant sur "split" pour partager l'écran de Grafana en choisissant Loki d'un côté et Prometheus de l'autre.

En image ça donne quelque chose comme ça :


Ce qu'on y voit ? La corrélation entre les logs Nginx et le load de la machine. Bon, certes, ce n'est pas foufou mais je n'avais pas d'exemple super pertinent à vous montrer. Quoi qu'il en soit, cette combinaison Loki/Prometheus vous permet de mettre en image ce que vous voulez et ainsi réagir comme il faut, le tout avec un peu de PromQL et des filtres.

Deux yeux, un affichage unifié, c'est franchement cool. Et je ne vous parle pas encore de l'extase si vous avez bien fait l'effort de faire correspondre les labels entre les deux outils...!

Perso, je trouve ça absolument génial et j'ai hâte de continuer à approfondir ma maîtrise de ces beautés !

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.

MariaDB en Master/Slave via ProxySQL

Rédigé par dada / 01 décembre 2018 / Aucun commentaire



On y est. J'ai enfin réussi à faire tourner ProxySQL comme je l'entendais pour que mes conteneurs tapent dans des BDD loin de Kubernetes. Sortir ses bases de données du cluster est un choix. Si vous êtes courageux, vous pouvez les laisser dedans. Ceci-dit, faut être un tantinet croyant pour se lancer dans la seconde solution. Dans ces histoires de hautes dispo et de résistance aux pannes, la bonne solution semble être de gérer son cluster de BDD en dehors de son cluster k8s.

Installation des MariaDB

Là, je devrais vous passer une pile de commandes pour installer des serveurs MariaDB en master/slave, mais bon. Tout est très clair dans la documentation officielle.

Sachez simplement que le master porte l'IP 192.168.0.17 et que le slave, lui, là 192.168.0.77.

Installation de ProxySQL

Helm ? Non, pas ici. On va donc pondre un YAML avec le Deployment et le Service qui va bien :
dada@master:~/proxysql$ cat proxysql.yaml

apiVersion: v1
kind: Deployment
metadata:
  name: proxysql
  labels:
    app: proxysql
spec:
  replicas: 2
  selector:
    matchLabels:
      app: proxysql
      tier: frontend
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: proxysql
        tier: frontend
    spec:
      restartPolicy: Always
      containers:
      - image: severalnines/proxysql:1.4.12
        name: proxysql
        volumeMounts:
        - name: proxysql-config
          mountPath: /etc/proxysql.cnf
          subPath: proxysql.cnf
        ports:
        - containerPort: 6033
          name: proxysql-mysql
        - containerPort: 6032
          name: proxysql-admin
      volumes:
      - name: proxysql-config
        configMap:
          name: proxysql-configmap
---
apiVersion: v1
kind: Service
metadata:
  name: proxysql
  labels:
    app: proxysql
    tier: frontend
spec:
  type: NodePort
  ports:
  - nodePort: 30033
    port: 6033
    name: proxysql-mysql
  - nodePort: 30032
    port: 6032
    name: proxysql-admin
  selector:
    app: proxysql
    tier: frontend

A remarquer

Le Deployment contient la configuration de ce qu'on appelle un ReplicatSet. Ça permet à k8s de toujours maintenir le nombre de pods déclaré en fonction. Quand un pod se casse la figure, il revient à lui. Dans un RS, quand un pod de casse la figure, il va aussi revenir à lui. Super. Sauf que, par exemple, dans le cas d'une mise à jour, k8s ne va pas terminer les pods d'un coup, mais les gérer un par un.

Le YAML contient aussi un Service de type NodePort qui va nous permettre de mapper les ports 30032 et 30033 internes aux pods sur les ports 6032 et 6033 accessibles depuis l'extérieur. Voyez la suite du billet pour découvrir qu'au lieu de taper sur le port 3306 de vos serveurs MariaDB, vous allez taper sur le port 6033.

On fait gober tout ça au cluster :
dada@master:~/proxysql$ kubectl apply -f proxysql.yaml
Et voilà le travail :
dada@master:~/proxysql$ kubectl get pods --all-namespaces  | grep proxysql
default            proxysql-5c47fb85fb-fdh4g                              1/1     Running     1          39h
default            proxysql-5c47fb85fb-kvdfv                              1/1     Running     1          39h

Configuration de ProxySQL

Tout va se jouer dans le fichier proxysql.cnf. Tout, ou presque, mais j'en parlerai après. Voici déjà de quoi faire :
datadir="/var/lib/proxysql"
admin_variables=
{
        admin_credentials="proxysql-admin:adminpwd"
        mysql_ifaces="0.0.0.0:6032"
        refresh_interval=2000
}
mysql_variables=
{
        threads=4
        max_connections=2048
        default_query_delay=0
        default_query_timeout=36000000
        have_compress=true
        poll_timeout=2000
        interfaces="0.0.0.0:6033;/tmp/proxysql.sock"
        default_schema="information_schema"
        stacksize=1048576
        server_version="5.1.30"
        connect_timeout_server=10000
        monitor_history=60000
        monitor_connect_interval=200000
        monitor_ping_interval=200000
        ping_interval_server_msec=10000
        ping_timeout_server=200
        commands_stats=true
        sessions_sort=true
        monitor_username="proxysql"
        monitor_password="proxysqlpwd"
}
mysql_replication_hostgroups =
(
        { writer_hostgroup=10, reader_hostgroup=20, comment="MariaDB Replication" }
)
mysql_servers =
(
        { address="192.168.0.17", port=3306, hostgroup=10, max_connections=100, max_replication_lag = 5 },
        { address="192.168.0.77", port=3306, hostgroup=20, max_connections=100, max_replication_lag = 5}
)
mysql_users =
(
        { username = "nextcloud" , password = "nextcloudpwd" , default_hostgroup = 10 , active = 1 }
)
mysql_query_rules =
(
        {
                rule_id=100
                active=1
                match_pattern="^SELECT .* FOR UPDATE"
                destination_hostgroup=10
                apply=1
        },
        {
                rule_id=200
                active=1
                match_pattern="^SELECT .*"
                destination_hostgroup=20
                apply=1
        },
        {
                rule_id=300
                active=1
                match_pattern=".*"
                destination_hostgroup=10
                apply=1
        }
)
Il est long alors on va le découper ensemble.

Global

{
        admin_credentials="proxysql-admin:adminpwd"
        mysql_ifaces="0.0.0.0:6032"
        refresh_interval=2000
}
Ici, on retrouve surtout les accès administrateur de ProxySQL et son port qui écoute sans limite d'IP.

Mysql_variables

mysql_variables=
{
        threads=4
        max_connections=2048
        default_query_delay=0
        default_query_timeout=36000000
        have_compress=true
        poll_timeout=2000
        interfaces="0.0.0.0:6033;/tmp/proxysql.sock"
        default_schema="information_schema"
        stacksize=1048576
        server_version="5.1.30"
        connect_timeout_server=10000
        monitor_history=60000
        monitor_connect_interval=200000
        monitor_ping_interval=200000
        ping_interval_server_msec=10000
        ping_timeout_server=200
        commands_stats=true
        sessions_sort=true
        monitor_username="proxysql"
        monitor_password="proxysqlpwd"
}
Là encore, je ne vais pas m'étendre sur toute la configuration (par défaut) mais sur deux points seulement : le port 6033 sera celui que vous allez rentrer dans les fichiers de configuration de vos services ayant besoin de MariaDB et l'utilisateur "monitor" est là pour vérifier l'état de vos serveurs MariaDB.

Mysql_replication_hostgroups

mysql_replication_hostgroups =
(
        { writer_hostgroup=10, reader_hostgroup=20, comment="MariaDB Replication" }
)
C'est à partir de là qu'on commence à s'amuser ! Ici, on déclare deux groupes de serveurs avec deux ID différents. Les writer, ceux qui ont le droit d'écrire et les reader, ceux qui n'auront que le droit de lire. Ça tombe bien, c'est plus ou moins ce qu'on veut.

Mysql_servers

mysql_servers =
(
        { address="192.168.0.17", port=3306, hostgroup=10, max_connections=100, max_replication_lag = 5 },
        { address="192.168.0.77", port=3306, hostgroup=20, max_connections=100, max_replication_lag = 5}
)
Si vous aviez suivi le blabla précédent, vous pouvez déchiffrer ces deux lignes en :

- Le serveur avec l'IP 192.168.0.17 fait partie du groupe des writers On lui demande de traiter 100 connexions actives maximum et d'avoir un lag avec son master de moins de 5 secondes.
- Le serveur avec l'IP 192.168.0.77 fait partie du groupe des reader. On lui demande de traiter 100 connexions actives maximum et d'avoir un lag avec son master de moins de 5 secondes.

Mysql_users

mysql_users =
(
        { username = "nextcloud" , password = "nextcloudpwd" , default_hostgroup = 10 , active = 1 }
)
Les utilisateurs MariaDB doivent être renseignés ici !
Dans mon cas, profitez de la configuration sécurisée de mon utilisateur nextcloud. Il sera assigné à un groupe d'utilisateur par défaut : le groupe des writer, et sera activé. Il est possible qu'un besoin particulier soit à l'origine de la création de la variable "active" mais j'ai du mal à comprendre pourquoi un compte MariaDB resterait renseigné avec active = 0 plutôt que dégagé. M'enfin.

Mysql_query_rules

mysql_query_rules =
(
        {
                rule_id=100
                active=1
                match_pattern="^SELECT .* FOR UPDATE"
                destination_hostgroup=10
                apply=1
        },
        {
                rule_id=200
                active=1
                match_pattern="^SELECT .*"
                destination_hostgroup=20
                apply=1
        },
        {
                rule_id=300
                active=1
                match_pattern=".*"
                destination_hostgroup=10
                apply=1
        }
Là, c'est la définition des règles qui seront automatiquement appliquées pour chaque serveur. La plus claire est la règle à l'ID 200 qui est assignée au hostgroup 20, celui des reader. C'est là que ProxySQL va faire un tri. Le hostgroup 20 n'ayant que la possibilité de faire des SELECT, on comprend bien qu'on parle de lecteurs uniquement.

ConfigMap

On charge tout ça dans une ConfigMap :
dada@master:~/proxysql$ kubectl create configmap proxysql-configmap --from-file=proxysql.cnf

Si on prend le temps de revenir sur le YAML pour comprendre la ConfigMap, on la repère ici :

      containers:
[...]
        volumeMounts:
        - name: proxysql-config
          mountPath: /etc/proxysql.cnf
          subPath: proxysql.cnf
[...]
      volumes:
      - name: proxysql-config
        configMap:
          name: proxysql-configmap

On comprend que les pods ProxySQL vont aller parcourir la liste des ConfigMaps disponibles pour repérer celle qui porte le nom "proxysql-config" et la monter dans /etc/proxysql.cnf.

Vérifier tout le système

Une commande que vous devriez connaître par cœur va nous prouver que tout fonctionne :

dada@master:~/proxysql$ kubectl logs proxysql-5c47fb85fb-fdh4g

Chez moi, elle sort quelque chose comme ça :

2018-12-01 08:30:19 [INFO] Dumping mysql_servers
+--------------+--------------+------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+-----------------+
| hostgroup_id | hostname     | port | weight | status | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment | mem_pointer     |
+--------------+--------------+------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+-----------------+
| 10           | 192.168.0.17 | 3306 | 1      | 0      | 0           | 100             | 5                   | 0       | 0              |         | 140637072236416 |
| 20           | 192.168.0.17 | 3306 | 1      | 0      | 0           | 100             | 5                   | 0       | 0              |         | 140637022769408 |
| 20           | 192.168.0.77 | 3306 | 1      | 0      | 0           | 100             | 5                   | 0       | 0              |         | 140637085320960 |
+--------------+--------------+------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+-----------------+

On y retrouve la liste des serveurs et leurs rôles : mon master appartient aux groupes reader et writer. Normal puisqu'il doit écrire et lire. Mon slave, lui, n'appartient qu'au groupe des reader, comme convenu.

Le piège

Si j'ai pesté un certain temps contre ProxySQL sur Mastodon , c'est que ma configuration ne marchait pas alors que tout semblait normal. J'avais bien en tête ce que je viens de vous expliquer: Les reader, les writer, la configuration des users, etc. C'était clair.
Pourtant, quand je lancais mon conteneur Nextcloud pour qu'il s'installe comme un grand, il pété ma réplication en écrivant sur mon slave. Le con.
J'ai fini par ouvrir une issue pour crier à l'aide et on m'a expliqué ceci : ProxySQL vérifie la présence de la variable READ_ONLY sur tous les serveurs renseignés dans la rubrique mysql_replication_hostsgroups. Si la variable est a OFF sur votre slave, la réplication entre les deux serveurs MariaDB ne bronchera pas, mais ProxySQL ira taper dans votre slave, peu importe la configuration en place. Re le con.

Pour vous épargner des soucis, pensez bien à vérifier que votre slave affiche bien ceci :
MariaDB [(none)]> SHOW VARIABLES like 'read_only';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| read_only     | ON   |
+---------------+-------+
1 row in set (0.00 sec)
Vous êtes bons. Vous pouvez maintenant aller configurer vos pods pour aller taper sur vos MariaDB via proxysql:6033 en place et lieu de localhost:3306.

Prometheus et Graphana pour voir dans son cluster Kubernetes

Rédigé par dada / 23 novembre 2018 / 2 commentaires


Mes aventures avec Kubernetes ne se sont pas arrêtées, loin de là. J'ai actuellement 54 pods répartis sur 3 nodes et 1 master et 2 VM MariaDB en master/slave, le tout reparti sur mon laptop, mon PC fixe et un vieux PC portable datant de ma vie étudiante. Je pourrais aussi vous parler de mon Pi-Hole sur ma Raspberry qui voit passer toutes les requêtes. Bref.
Aujourd'hui, alors que je peste contre ProxySQL sur Mastodon, j'ai décidé de regarder du côté du monitoring de l'orchestrateur, histoire de prendre l'air. Si vous êtes un malheureux lecteur régulier, vous devriez savoir que j'adore Prometheus et que Grafana fait, à mes yeux, des dessins bien plus impressionnants que Klimt.

Note : ça n'a pas calmé ma frustration : c'est très rapide et simple à mettre en place.

Installer Prometheus

Avec pour base mes billets précédents, nous allons installer Prometheus, Grafana et l'AlertManager avec Helm.

Ajouter le dépôt dans helm

Là, à la manière d'APT, nous allons ajouter une source à Helm pour lui permettre d'installer ce qu'on lui demande.

helm repo add coreos https://s3-eu-west-1.amazonaws.com/coreos-charts/stable/

Installer l'operator

On enchaîne sur l'installation de l'operator, le grand patron qui va s'occuper pour nous de Prometheus dans k8s.

helm install coreos/prometheus-operator --name prometheus-operator --namespace monitoring
Une fois exécutée, vous devriez voir ça :
dada@master:~/prometheus$ kubectl get pods -n monitoring -o wide
NAME                                                   READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE
alertmanager-kube-prometheus-0                         2/2     Running   0          37m   10.244.3.140   node3    <none>
kube-prometheus-exporter-kube-state-65b6dbf6b4-b52jl   2/2     Running   0          33m   10.244.3.141   node3    <none>
kube-prometheus-exporter-node-5tnsl                    1/1     Running   0          37m   192.168.0.76   node1    <none>
kube-prometheus-exporter-node-fd7pt                    1/1     Running   0          37m   192.168.0.49   node3    <none>
kube-prometheus-exporter-node-mfdj2                    1/1     Running   0          37m   192.168.0.22   node2    <none>
kube-prometheus-exporter-node-rg5q6                    1/1     Running   0          37m   192.168.0.23   master   <none>
kube-prometheus-grafana-6f6c894c5b-2d6h4               2/2     Running   0          37m   10.244.2.165   node2    <none>
prometheus-kube-prometheus-0                           3/3     Running   1          37m   10.244.1.187   node1    <none>
prometheus-operator-87779759-wkpfz                     1/1     Running   0          49m   10.244.1.185   node1    <none>

Notez que si vous êtes, comme moi, sur une connexion ADSL classique, vous aller avoir le temps d'aller faire couler un grand café et d'aller le boire une clope au bec et au soleil. Votre cluster va télécharger beaucoup de pods et sur chaque nœud.

En y regardant bien, on retrouve :

  • L'AlertManager permettant de nous spammer en cas de souci,
  • Les exporter nodes placés logiquement sur chaque node et sur notre master,
  • Grafana, le copain de Prometheus qui vous fait des beaux dessins,
  • Le kube et l'operator.

Accéder à tout ça

L'installation est vraiment triviale. Le petit bonus de ce billet sera de vous passer une liste de commandes pour admirer le tout dans votre navigateur préféré : Firefox.

Pour accéder à l'interface de Prometheus

Commencez par ouvrir un tunnel SSH sur le port 9090 vers votre master :

ssh -L 9090:127.0.0.1:9090 dada@IPDuMaster

Puis lancez le port-foward :

kubectl port-forward -n monitoring prometheus-prometheus-operator-prometheus-0 9090

Pour accéder à l'interface de Grafana

Encore un tunnel SSH, sur le 3000 ce coup-ci :

ssh -L 3000:127.0.0.1:3000 dada@IPDuMaster

Et encore un port-forward :

kubectl port-forward $(kubectl get  pods --selector=app=grafana -n  monitoring --output=jsonpath="{.items..metadata.name}") -n monitoring  3000

Vous êtes bons ! Les dashboards sont maintenant accessibles en tapant http://localhost:PORT dans Firefox.

En image, ça devrait donner ça pour Grafana :

Et ça pour les alertes Prometheus :

Alors, oui. Vous avez aussi remarqué que des alertes étaient déjà levées ? Ce sont des outils/configurations que Prometheus attend de rencontrer dans votre cluster. Le mien n'a pas encore ces histoires de scheduler ou de controller manager. Ça va faire partie des découvertes à suivre dans les futurs billets.

Des bisous !

Et si on mettait PluXml dans un cluster k8s ?

Rédigé par dada / 11 novembre 2018 / Aucun commentaire



L'idée est saugrenue mais c'est exactement ce que j'ai fait ces derniers temps. Non sans souffrir, j'ai réussi à m'en sortir. Au programme : créer un conteneur qui va bien pour PluXml, mettre en place un déploiement pour le faire tourner dans k8s et l'utilisation de PersistentVolume.

Notez que je découvre toujours cet univers et que ce qui est présenté dans ce billet ne reflète que mes avancées.

La création du conteneur

Introduction

J'ai déjà pondu quelques conteneurs, principalement des exporters Prometheus en Python qui vont récupérer les statistiques de Mastodon et Pixelfed pour les afficher dans des dashboards Grafana (combo mots clés pour des recruteurs).

Pour PluXml, c'est différent. Il nous faut un conteneur avec :
- Les sources du CMS
- Du php7 et ses dépendances
- Un serveur web

Voyons comment y arriver en s'inspirant de ce qui a été fait pour Wordpress.

Le Dockerfile

C'est quoi ? C'est le fichier de configuration qui va nous permettre de créer le conteneur. En gros, les sources du conteneur.
FROM php:7.0-apache

WORKDIR /var/www/html

RUN apt update
RUN apt install -y wget unzip
RUN wget https://git.dadall.info/dada/pluxml/raw/master/pluxml-latest.zip
RUN mv pluxml-latest.zip /usr/src/

#VOLUME
VOLUME /var/www/html

RUN a2enmod rewrite
RUN service apache2 restart
RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
    && docker-php-ext-install -j$(nproc) iconv \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install -j$(nproc) gd

# Expose
EXPOSE 80

COPY entrypoint.sh /usr/local/bin/

ENTRYPOINT ["entrypoint.sh"]
CMD ["apache2-foreground"]

Comprendre le Dockerfile

FROM : C'est ici qu'on indique clairement la solution de facilité. Pour ne pas se battre avec l'installation d'Apache et de PHP7, j'indique au Dockerfile de prendre comme base un conteneur ayant déjà PHP7, Apache et Debian configurés.

WORKDIR : Ici, on indique où vont s’exécuter les commandes qui vont suivre. Ce n'est pas pertinent dans mon cas, mais bon.

VOLUME : C'est une sorte de point de montage entre le conteneur et son hôte.

RUN : Simple : les commandes que nous allons exécuter à la création du conteneur. Ici, on va récupérer les sources de PluXml qui sont hébergées dans mon Gitlab perso, les extraire et les préparer. On va aussi activer le module Apache nécessaire au bon fonctionnement du CMS et installer le module PHP-GD, lui aussi obligatoire.

EXPOSE : C'est le port qui sera ouvert vers l'extérieur. On va taper sur le port 80 pour accéder au contenu du conteneur.

COPY : Commande permettant de copier des fichiers de votre ordinateur à l'intérieur du conteneur. ici, le script entrypoint.

CMD : La commande à exécuter une fois que tout est terminé.

Note : Oui, je sais, il y a des choses inutiles. C'est un exemple.

Entrypoint ?

Le script :
#!/bin/bash

if [ ! -e index.php ]; then
    unzip /usr/src/pluxml-latest.zip -d /var/www/html/
    mv /var/www/html/PluXml/* /var/www/html
    rm -rf /var/www/html/PluXml
    chown -R www-data: /var/www/html
fi

exec "$@"

Comprendre le entrypoint

On est face à un script bash tout simple mais il demande un peu d'explication quand même.
Nous voulons un conteneur mettant à disposition les sources de notre CMS adoré ET qui stockera les articles, commentaires, images, etc, loin de ce-dit conteneur. Sans ça, une fois le conteneur mort, les données mourront avec lui. Ces données, pour faire simple, seront dans le fameux VOLUME cité un peu plus haut.

Au sujet des sources, si notre Dockerfile s'amusait à les mettre directement dans /var/www/html, comme la norme le voudrait, elles disparaîtraient dans k8s.
Pourquoi ? Parce que ce VOLUME sera géré par k8s lui-même et sera vide :
- Dockerfile dépose les sources dans le conteneur
- k8s charge le conteneur
- k8s monte son persistentVolume dans /var/www/html
- Les sources du conteneur disparaissent. Échec.

Si vous regardez bien le Dockerfile et ses incroyablement nombreuses lignes de code : il déplace les sources dans /usr/src, bien au chaud, à l’abri du problème. À la création du conteneur, dans k8s, le script regardera s'il existe un fichier index.php (issu des sources du PluXml) et, le cas échéant, copiera les fichiers manquants dans /var/www/html qui sera un PersistentVolume dans k8s ! Et c'est bien ce que nous voulons !

La création de l'image

Pour construire une image Docker, placez-vous dans les répertoires où vous avez déposé les 2 fichiers détaillés précédemment et :
docker build -t pluxml-5.6 .
Vous allez voir des lignes s'afficher dans tous les sens : pas de panique, c'est normal. Une fois votre terminal calmé :
Successfully built d554d0753425
Successfully tagged pluxml-5.6:latest
Gagné !

Téléverser l'image dans un dépôt

On ne va pas s'attarder là-dessus. Sachez juste que pour que k8s se serve de l'image, il faut qu'elle soit téléchargeable. J'ai poussé mon image dans le Docker Hub, dans un dépôt public. Vous allez pouvoir jouer avec sans vous prendre la tête.

Préparer Kubernetes

Postulats de base :
- Vous avez configuré Rook.
- Vous avez dégagé ce qui concerne les pods Wordpress/Mysql.

Le deployment & co

On va reprendre le travail vu ces derniers jours et créer des YAML pour notre conteneur !
dada@k8smaster:~/pluxml$ cat pluxml.yaml 
apiVersion: v1
kind: Service
metadata:
  name: pluxml
  labels:
    app: pluxml
spec:
  ports:
  - port: 80
  selector:
    app: pluxml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: plx-pv-claim
  labels:
    app: pluxml
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: pluxml
  labels:
    app: pluxml
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: pluxml
    spec:
      containers:
      - image: dadall/pluxml-5.6:latest
        imagePullPolicy: "Always"
        name: pluxml
        ports:
        - containerPort: 80
          name: pluxml
        volumeMounts:
        - name: pluxml-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: pluxml-persistent-storage
        persistentVolumeClaim:
          claimName: plx-pv-claim
Vous remarquez que je ne me suis pas foulé : c'est le YAML de Wordpress légèrement retravaillé. On y trouve le nom de mon image dans le Deployment et un Service pluxml.

L'ingress

dada@k8smaster:~/pluxml$ cat pluxml-ingress.yaml 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: pluxml-ingress
spec:
 backend:
   serviceName: pluxml
   servicePort: 80
Là aussi, simple : l'ingress qui s'attache à l'application pluxml.

Vous créez tout ça :
dada@k8smaster:~/pluxml$ kubectl create -f pluxml.yaml
dada@k8smaster:~/pluxml$ kubectl create -f pluxml-ingress.yaml
Si tout va bien, vous avez un pod PluXml qui vient d'apparaitre et qui est joignable !

Le pod :
dada@k8smaster:~/pluxml$ kubectl get pods --all-namespaces -o wide | grep plux
default            pluxml-686f7d486-7p5sq                                    1/1     Running     0          82m     10.244.2.164   k8snode2    <none>
Le service :
dada@k8smaster:~/pluxml$ kubectl describe svc pluxml
Name:              pluxml
Namespace:         default
Labels:            app=pluxml
Annotations:       <none>
Selector:          app=pluxml
Type:              ClusterIP
IP:                10.100.177.201
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.244.1.31:80
Session Affinity:  None
Events:            <none>
L'Ingress :
dada@k8smaster:~/pluxml$ kubectl describe ingress pluxml
Name:             pluxml-ingress
Namespace:        default
Address:         
Default backend:  pluxml:80 (10.244.1.31:80,10.244.2.164:80)
Rules:
  Host  Path  Backends
  ----  ----  --------
  *     *     pluxml:80 (10.244.1.31:80,10.244.2.164:80)
Annotations:
  kubernetes.io/ingress.class:  nginx
Events:                         <none>

Coucou PluXml !