Hello les amis, est-ce qu’on n’a pas envie d’un burrito aux verres pilés et aux orties assaisonnées à la sauce piquante aujourd’hui ?

J’ai envie de challenger une idée qui prend la poussière dans ma tête depuis un certain temps.

Faire un setup douloureux et laborieux de MongoDB en cluster en mode replicaset dans trois modes possibles.

  • Un cluster en mode compute traditionnel, installation de binaires en dur et maintenance classique en mode admin sys devant l’éternel.
  • Un cluster sur Kubernetes, pour challenger les problématiques liées à la complexité de Kubernetes.
  • Un cluster en mode compute GCE et container, un mélange de l’inflexibilité du compute et la facilité des containers.

L’idée étant de repousser, valider ou réfuter ce que j’entends depuis longtemps au sujet de la gestion des charges de travail “sensibles”.

Comme les bases de données ou les trucs stateful hyper radioactif comme Jenkins.

Image d’un dragon à 3 têtes illustrant un cluster MongoDB. La première tête sérieuse est le master node, la deuxième tête sérieuse est le replica 1, la troisième tête avec un air farceur est le replica 2 en erreur de synchronisation

Quelques idées à considérer :

  • Les containers offrent un gain de performance par rapport à un compute brut.
  • Docker ajoute une complexité inutile.
  • Les modifications manuelles sont mal tolérées.
  • Moins fiable qu’un compute classique.
  • Kubernetes est trop complexe pour gérer des charges stateful.
  • Il n’est pas recommandé de faire tourner une base de données dans Kubernetes, risque de perte de performances avec le partage des ressources.

Cluster Compute + Container

L’idée derrière ce setup est de comprendre la possibilité offerte par GCP de porter un container unique dans un compute.

Ce n’est pas une solution évidente et peu mise en avant par Google. Dans l’UI, on peut lancer des containers au démarrage en utilisant le mode init script. C’est mal documenté, limité à un container unique, et la syntaxe ressemble à du Docker Compose mais en moins bien.

Pourquoi s’acharner sur cette approche ?

  • Alternative entre la gestion d’une charge stateful sur Kube et l’installation d’un compute en dur.
  • Pas besoin d’un cluster Kube et de sa complexité.
  • Toute la puissance du compute sans partage de ressources lié à Kube.
  • Sécurité assurée avec un lancement en mode non privilégié et cloisonné dans un container.
  • Utilisation de l’image système durcie de GCP, éliminant la gestion de l’hôte.
  • Compute en temps long, moins de perturbations et pas de soucis de montée de version de Kubernetes.
  • Maintenance quotidienne plus simple (mise à jour du container, redimensionnement et snapshot facile des disques).

Meme, OK let’s go (passionné de fête foraine)

Setup cluster compute avec container

Je ne vais pas vous assommer le crâne en vous fournissant toute la configuration Terraform que j’ai mise en place, cela serait indigeste. Cependant, je vais vous donner les astuces essentielles que j’ai mises en place pour réussir mon setup.

Voici les points sur lesquels j’ai concentré mes efforts pour obtenir un cluster respectant les bonnes pratiques.

  • Les nœuds ont des adresses IP privées mais pas d’adresses IP publiques (j’utilise Cloud IAP SSH pour l’accès aux machines).
  • J’ai créé un VPC + un sous-réseau avec un NAT dans une région (le NAT est utile pour les patchs de sécurité des machines).
  • J’ai mis en place des disques de données en plus des disques système, directement attachés au conteneur.
  • J’ai configuré une règle de pare-feu pour restreindre l’accès à MongoDB aux computes sur le réseau.
  • J’ai dû bricoler l’image officielle Mongodb pour pouvoir écrire les fichiers de la base de données sur le disque attaché.
  • J’ai installé l’agent Compute pour obtenir les métriques de RAM de ma machine (très important pour une base de données).
  • J’utilise l’image COS (Container-Optimized OS) pour réduire la surface d’attaque.
  • Je mets en place des snapshots journaliers de mes disques de données.
  • J’ai ajouté un utilisateur et un mot de passe sur le service MongoDB, mais je n’ai pas fait d’effort pour le sécuriser et le stocker dans un secret GCP.
    • Je pars du principe que cela va de soi de ne pas laisser un mot de passe faible et de le protéger.

En tout état de cause, je voulais commencer par le cluster en mode compute, mais finalement j’ai dévié et j’ai fait le compute + conteneur en premier.

Heureusement, je n’envoie pas des gros riches dans l’espace, donc si ça drift, je ne suis pas interdit de vols, n’est-ce pas Virgin Galactic? :)

Photo du vaisseau spatial de Virgin Galactic

Bricolage de l’image Docker

J’utilise une image officielle Bitnami MongoDB pour créer mon cluster. Lien vers le Dockerfile

Comme j’ai besoin d’un point de montage de disque et de donner les permissions de lecture/écriture sur le disque, je suis obligé de créer une image dérivée avec un volume et une gestion des permissions au runtime. J’ai rencontré des problèmes avec l’image Bitnami.

1
2
3
4
5
6
7
8
FROM mongodb/mongodb-community-server:7.0.4-ubuntu2204
USER root
RUN apt update && apt upgrade -y
RUN apt install gosu -y
COPY entrypoint-rewrite.sh entrypoint-rewrite.sh
COPY key_file.key key_file.key
RUN chmod +x entrypoint-rewrite.sh
ENTRYPOINT ["/entrypoint-rewrite.sh"]

l’image représente un vol de container en mer, des pirates ont chargé un container sur un bateau inadapté. la legende de l’image the docker container: my host machine

l’entrypoint du container avec modifications des permissions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

function init_mongo_cluster(){
if [[ ! -z "$INIT_CLUSTER_COMMAND_B64" ]]; 
then
  echo "wait for service mongo to run"
  sleep 15
  eval $(echo "$INIT_CLUSTER_COMMAND_B64"|base64 -d)
fi
}

set -x 
chown -R mongodb:mongodb /data

echo "$SSL_CLUSTER_KEY_B64"|base64 -d > /key_file.key
chmod 400 /key_file.key
chown -R mongodb:mongodb /key_file.key
mkdir -p /home/mongodb
chown -R mongodb:mongodb /home/mongodb

init_mongo_cluster &
exec gosu mongodb python3 /usr/local/bin/docker-entrypoint.py --replSet dbrs --bind_ip_all --keyFile /key_file.key

Petite astuce, mon utilisateur principal dans le conteneur est root, sinon je ne peux pas modifier les permissions d’écriture sur disque au runtime. J’utilise le binaire gosu pour que le processus MongoDB puisse être lancé avec l’utilisateur non privilégié mongodb. Donc en somme, l’utilisateur principal est root dans le conteneur, mais le service mongo est lancé avec l’utilisateur mongo.

J’ai besoin d’une clé SSL que je passe en variable d’environnement sur les 3 computes. L’initialisation du cluster se fera uniquement dans le nœud master via la variable d’environnement INIT_CLUSTER_COMMAND_B64.

Enfin, j’aménage mon conteneur tranquillou, rien de trop suspect :)

Dexter: l’agent Doakes surprend Dexter qui fouille dans un conteneur, avec la légendaire réplique “Surprise, motherfucker!

Donc je vais quand même vous mettre un peu de Terraform pour vous montrer les quelques cailloux que j’ai croisés sur ma route.

Le premier challenge auquel j’ai fait face, c’était de comprendre comment fonctionne la syntaxe Terraform pour lancer des conteneurs dans des computes.

Mon approche, c’est de configurer un compute avec une configuration la plus proche possible de mon besoin et profiter d’une feature de GCP qui donne une configuration Terraform statique du compute.

Une fois que j’ai cette configuration minimale, j’adapte à mon besoin et je variabilise ma configuration pour mes besoins.

Une fois que j’ai eu un compute MongoDB unique fonctionnel, le jeu va être de dupliquer x2 la même conf et faire une exception pour les variables du premier nœud qui sera le master, pour avoir une configuration master et deux replica.

Ça nous donne un truc comme ça:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
resource "google_project_iam_member" "mongocluster_iam_bindind" {
  for_each = toset(local.regions)
  project  = var.project
  role     = "roles/artifactregistry.reader"
  member   = "serviceAccount:${google_service_account.sa_mongo[each.key].email}"
}

resource "google_service_account" "sa_mongo" {
  for_each     = toset(local.regions)
  account_id   = "sa-mongo-${var.region}-${each.key}"
  display_name = "Service Account mongo-compute-${var.region}-${each.key}"
}

resource "google_compute_instance" "mongo-compute" {
  for_each = toset(local.regions)
  name     = "mongo-compute-${var.region}-${each.key}"

  tags = ["mongodb", "allow-ssh"]

  attached_disk {
    device_name = data.google_compute_disk.mongo-data[each.key].name
    mode        = "READ_WRITE"
    source      = data.google_compute_disk.mongo-data[each.key].id
  }

  boot_disk {
    auto_delete = true
    device_name = "mongo-compute-${var.region}-${each.key}"

    initialize_params {
      image = "cos-cloud/cos-105-lts"
      size  = 100
      type  = "pd-balanced"
    }

    mode = "READ_WRITE"
  }

  can_ip_forward      = false
  deletion_protection = false
  enable_display      = false

  machine_type = "e2-medium"

  metadata = {
    google-monitoring-enabled = "true"
    gce-container-declaration = templatefile("${path.module}/container_configuration.yaml.tpl", {
      container_name = "mongo-compute-${var.region}-${each.key}",
      image          = var.image,
      pdName         = data.google_compute_disk.mongo-data[each.key].name,
      env_vars       = local.env_vars[each.key],
    })

  }

  network_interface {
    subnetwork = data.google_compute_subnetwork.mongodb_subnetwork.id
    network_ip = data.google_compute_address.internal_ips[each.key].address
  }

  scheduling {
    automatic_restart   = true
    on_host_maintenance = "MIGRATE"
    preemptible         = false
    provisioning_model  = "STANDARD"
  }

  service_account {
    email = google_service_account.sa_mongo[each.key].email

    scopes = ["https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring.write",
      "https://www.googleapis.com/auth/service.management.readonly",
      "https://www.googleapis.com/auth/servicecontrol",
    "https://www.googleapis.com/auth/trace.append"]
  }

  shielded_instance_config {
    enable_integrity_monitoring = true
    enable_secure_boot          = false
    enable_vtpm                 = true
  }

  zone = "${var.region}-${each.key}"
}
i

  • J’ai mis les disques persistants et les IP privées réservées dans un autre dossier.
  • J’ai activé l’agent de monitoring.
  • J’ai templatisé plus proprement le YAML de configuration du conteneur.

J’ai récupéré cette configuration en dur dans l’interface de GCP et changé mes trucs dynamiquement.

Buzz l’Éclair, Terraform everywhere

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
spec:
  containers:
  - name: ${container_name}
    image: ${image} 
    env:
${join("\n", [
      for key, value in env_vars : "    - name: ${key}\n      value: ${value}"
    ])}
    volumeMounts:
    - name: pd-0
      readOnly: false
      mountPath: /data
    securityContext:
      runAsUser: 1001
      privileged: false
    stdin: false
    tty: false
  volumes:
  - name: pd-0
    gcePersistentDisk:
      pdName: ${pdName}
      fsType: ext4
      partition: 0
      readOnly: false
  restartPolicy: Always
# This container declaration format is not public API and may change without notice. Please
# use gcloud command-line tool or Google Cloud Console to run Containers on Google Compute Engine.

Cela me permet de variabiliser l’image de mon conteneur et de faire une boucle sur les variables d’environnement.

J’utilise l’utilisateur 1001 (mongodb), et mon conteneur écrira sur /data sur le disque attaché.

Et bien sûr, les variables d’environnement qui vont bien pour configurer mon cluster.

Développeur confus par une variable d’environnement de base de données

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
locals {
  regions = ["b", "c", "d"]

  common_vars = {
    MONGODB_EXTRA_FLAGS       = "--wiredTigerCacheSizeGB=2"
    MONGODB_INITDB_ROOT_USERNAME = "root"
    MONGODB_INITDB_ROOT_PASSWORD = "password"
    MONGODB_USERNAME           = "mongo"
    MONGODB_PASSWORD           = "password"
    MONGODB_DATABASE           = "user"
    SSL_CLUSTER_KEY_B64        = "YnNGU0FxM3Y5ZC9vYTNCTzI3Q1IxVTFzekpvdEt3TkF6L(troncate)"
  }

  region_config = {
    b = {
      MONGODB_REPLICA_SET_MODE     = "primary"
      MONGODB_INITIAL_PRIMARY_HOST = data.google_compute_address.internal_ips["b"].address
      MONGODB_ADVERTISED_HOSTNAME  = data.google_compute_address.internal_ips["b"].address
      # saut de ligne parceque la commande est très longue :)
      INIT_CLUSTER_COMMAND_B64     = base64encode("mongosh -u root -p password --eval \"rs.initiate(
{_id:'dbrs', members: [{_id:0, host: '${data.google_compute_address.internal_ips["b"].address}'}, 
{_id:1, host: '${data.google_compute_address.internal_ips["c"].address}'},
{_id:2, host: '${data.google_compute_address.internal_ips["d"].address}'}]})\"")

    },
    c = {
      MONGODB_REPLICA_SET_MODE     = "secondary"
      MONGODB_INITIAL_PRIMARY_HOST = data.google_compute_address.internal_ips["b"].address
      MONGODB_ADVERTISED_HOSTNAME  = data.google_compute_address.internal_ips["c"].address
    },
    d = {
      MONGODB_REPLICA_SET_MODE              = "secondary"
      MONGODB_INITIAL_PRIMARY_HOST          = data.google_compute_address.internal_ips["b"].address
      MONGODB_ADVERTISED_HOSTNAME           = data.google_compute_address.internal_ips["d"].address
    }
  }

  env_vars = { for region, config in local.region_config : region => merge(local.common_vars, config) }
}

Femme qui se frotte les yeux

Donc là, on a un truc bien lourd, comme une mauvaise blague de Bigard. Je vais tâcher de vous expliquer comment j’en suis arrivé là.

On a besoin de variables communes entre les trois computes, d’un mode de cluster (primary/secondary), de l’adresse IP du primary et des adresses IP des secondary. INIT_CLUSTER_COMMAND_B64 est lancé uniquement dans le conteneur master, pour finir de configurer le cluster, et on a besoin d’une clé SSL pour autoriser la communication entre les nœuds du cluster.

Alors bien sûr, je ne suis pas dans un cluster de prod, donc j’ai mis un mot de passe complètement pété, ne m’en voulez pas, ce n’était pas ma priorité :) La clé SSL en dur dans les variables, même combat, désolé mais grosse flemme ! J’abstrais de la complexité de créer un mot de passe sécurisé en Terraform et de le mettre dans le vault de GCP. Dans un contexte de prod, j’aurais pris le temps de le faire.

Petite astuce, si je change la version de mon YAML GCE, il ne sera pas rafraîchi dans le compute. C’est chiant la plupart du temps (j’ai un workaround plus tard pour ça), mais comme on veut contrôler le comportement de notre cluster et les mises à jour de nos images, c’est arrangeant dans notre contexte d’exécution du cluster. Comme on doit mettre à jour le master en premier, s’assurer que le démarrage est bon et mettre à jour les replicas un par un ensuite, la dernière chose qu’on veut, c’est les 3 nœuds du cluster qui se mettent à jour en même temps.

Pour remplacer un nouveau conteneur, en cas de changement de metadata, il faut lancer la commande sudo systemctl start konlet-startup. Le service Konlet est responsable des mises à jour de conteneurs sur les images Chrome OS sur GCP, et ce service vient lire les metadata pour lancer le conteneur et son environnement.

Alors bien sûr, on se doute bien que je n’ai pas obtenu ce résultat du premier coup, j’ai quand même cogné la tête quelques fois sur mon clavier !

Maintenant que le cluster fonctionne, on va jouer avec, pour démontrer qu’il fonctionne, mais que le code doit s’adapter aussi à cette contrainte de cluster.

Park and Rec: Leslie Knope Everything hurts and I’m dying

Un script Python pour simuler des lectures/écritures de données

Je ne me suis pas trop cassé la tête, j’ai construit ça dans un Docker et livré ça dans un compute en mode conteneur (tant qu’à faire, autant réutiliser le même concept). Le but, c’est de simuler une application qui fait des lectures/écritures sur la base de données en boucle, pour simuler une utilisation raisonnable du cluster.

Voici mon script, assisté par ChatGPT, qui ne voulait pas me faire un script avec de fausses données, mais que j’ai réussi à “convaincre” que je ne l’utiliserais pas en prod :)

Au final, la gestion des erreurs de connexion et la haute disponibilité ajoutent pas mal de complexité à ce script, mais c’est nécessaire pour vérifier que la haute dispo du cluster fonctionne.

Lara Croft combat un anaconda dans la jungle

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
# coding:utf8

import pymongo
import os
import sys
import time
import random
import string
from faker import Faker
import logging
import pprint

# Configure the logging module
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s]: %(message)s",
    handlers=[logging.StreamHandler()],
)

# List of required environment variables
required_env_vars = [
    "MONGO_URI",
    "MONGO_DATABASE",
    "MONGO_COLLECTION",
    "MONGO_REPLICASET_NAME",
]

# Check if all required environment variables are set
if not all(os.environ.get(var) for var in required_env_vars):
    missing_vars = [var for var in required_env_vars if not os.environ.get(var)]
    logging.error("Error: Please set the following environment variables:")
    for var in missing_vars:
        logging.error(f"- {var}")
    sys.exit(1)

# Read MongoDB connection details from environment variables
mongo_uri = os.environ["MONGO_URI"]
database_name = os.environ["MONGO_DATABASE"]
collection_name = os.environ["MONGO_COLLECTION"]
replicaset_name = os.environ["MONGO_REPLICASET_NAME"]

# Create a Faker instance to generate user data
fake = Faker()

# Revised connect_to_mongodb function with a 5-second timeout
def connect_to_mongodb():
    while True:
        try:
            client = pymongo.MongoClient(
                mongo_uri,
                replicaSet=replicaset_name,
                readPreference="secondaryPreferred",
                serverSelectionTimeoutMS=5000,  # 5-second timeout
            )
            return client
        except pymongo.errors.ConnectionFailure as err:
            logging.error(err)
            logging.error("Connection to MongoDB failed. Retrying in 5 seconds...")
            time.sleep(5)

# Revised perform_mongodb_operation function with additional checks for primary server availability
def perform_mongodb_operation(operation, data):
    while True:
        try:
            client.admin.command('ismaster')  # This checks for the primary server
            operation(data)
            return
        except (pymongo.errors.AutoReconnect, pymongo.errors.PyMongoError.timeout) as err:
            logging.error(err)
            logging.error(f"{operation.__name__} operation failed. Retrying in 5 seconds...")
            time.sleep(5)

# Infinite loop for continuous write and read
while True:
    client = connect_to_mongodb()
    db = client[database_name]
    collection = db[collection_name]

    user_data = {
        "_id": "".join(random.choices(string.ascii_letters + string.digits, k=8)),
        "name": fake.name(),
        "email": fake.email(),
        "address": fake.address(),
        "phone_number": fake.phone_number(),
        "company": fake.company(),
        "job_title": fake.job(),
    }

    logging.info(pprint.pformat(user_data))

    # Measure the start time before writing
    write_start_time = time.time()

    # Updated retry logic for write operation
    perform_mongodb_operation(collection.insert_one, user_data)

    # Measure the write time
    write_time = time.time() - write_start_time
    logging.info("Write Time: %s seconds", write_time)

    # Measure the start time before reading
    read_start_time = time.time()

    # Simplified retry logic for read operation
    perform_mongodb_operation(collection.find_one, {"_id": user_data["_id"]})

    # Measure the read time
    read_time = time.time() - read_start_time
    logging.info("Read Time: %s seconds", read_time)

    # Ensure not to overload MongoDB by waiting before the next iteration
    time.sleep(1)

Rien de trop spécial dedans, on récupère les infos de connexion à MongoDB dans les variables d’environnement, et on écrit un faux utilisateur qu’on lit tout de suite après, dans une boucle infinie. Le script gère une reconnexion en cas d’interruption, et on imprime les durées de lecture/écriture.

Le but est de logger les interruptions et de voir comment se comporte notre cluster aussi bien en lecture qu’en écriture.

L’OS agent expose les métriques de RAM que l’on peut rajouter dans un tableau de bord personnalisé. Ce qui va nous intéresser, c’est de voir comment les computes containers se comportent dans un contexte de failover.

J’ai repris le même système de compute container, et l’astuce pour forcer la création d’une nouvelle machine, c’est de changer la description de la machine. Du coup, je mets l’image Docker dans la description du compute comme ceci description = "run ${var.image}"

On casse et on regarde comment ça se passe

Donc là, j’ai mon cluster master + deux replicas, ce qui nous donne ceci. Une machine par zone différente, des IP fixées qui se suivent, mongo-compute-europe-west1-b est le nœud master par défaut.

1
2
3
mongo-compute-europe-west1-b    europe-west1-b      10.2.0.21 (nic0)
mongo-compute-europe-west1-c    europe-west1-c      10.2.0.22 (nic0)
mongo-compute-europe-west1-d    europe-west1-d      10.2.0.20 (nic0)

Voici mes 3 machines, en fin de setup le master est toujours mongo-compute-europe-west1-b (c’est mes variables d’env qui imposent ça).

Je me connecte dans mon master et dans mon conteneur pour donner le statut de mon cluster. On voit bien le master et les deux replicas dans leur fonctionnement initial.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[...]
  members: [
    {
      _id: 0,
      name: '10.2.0.21:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 79,
    },
    {
      _id: 1,
      name: '10.2.0.22:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 66,
[...]
    },
    {
      _id: 2,
      name: '10.2.0.20:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 66,
[...]
}]

Voici les logs de mon script, qui fait tranquillement ses lectures écritures.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 'address': '0343 Wright Mountains\nNew Brucechester, NC 70886',
 'company': 'Delgado Ltd',
 'email': '[email protected]',
 'job_title': 'Forensic psychologist',
 'name': 'Mrs. Michelle Wagner',
 'phone_number': '001-482-908-4597x7168'}
2023-12-20 14:53:25,752 [INFO]: Write Time: 0.016977548599243164 seconds
2023-12-20 14:53:25,755 [INFO]: Read Time: 0.0035524368286132812 seconds
2023-12-20 14:53:26,758 [INFO]: {'_id': 'zmuuSgVc',
 'address': 'Unit 7165 Box 6751\nDPO AE 34938',
 'company': 'Mayo Inc',
 'email': '[email protected]',
 'job_title': 'Radiation protection practitioner',
 'name': 'Troy Werner',
 'phone_number': '724.210.0471'}
2023-12-20 14:53:26,778 [INFO]: Write Time: 0.019673824310302734 seconds
2023-12-20 14:53:26,782 [INFO]: Read Time: 0.0033690929412841797 seconds
2023-12-20 14:53:27,785 [INFO]: {'_id': 'lvwo69eZ',
 'address': '166 Joshua Parkways Apt. 547\nNorth Carmen, PR 69738',
 'company': 'Estrada and Sons',
 'email': '[email protected]',
 'job_title': 'Child psychotherapist',
 'name': 'Jamie Robertson',
 'phone_number': '502-440-3565'}
2023-12-20 14:53:27,803 [INFO]: Write Time: 0.018307209014892578 seconds
2023-12-20 14:53:27,806 [INFO]: Read Time: 0.003098726272583008 seconds
2023-12-20 14:53:28,809 [INFO]: {'_id': '1GdHyiVP',
 'address': '0989 Mark Spur\nBergside, MP 55099',
 'company': 'Williams, Perkins and Schwartz',
 'email': '[email protected]',
 'job_title': 'Risk manager',
 'name': 'Kathryn Hickman',
 'phone_number': '(645)935-5258'}
2023-12-20 14:53:28,828 [INFO]: Write Time: 0.01801753044128418 seconds
2023-12-20 14:53:28,831 [INFO]: Read Time: 0.003077983856201172 seconds
2023-12-20 14:53:29,834 [INFO]: {'_id': 'GEeXQ2dg',
 'address': '920 Peters Camp\nNew Heather, UT 80110',
 'company': 'Hahn and Sons',
 'email': '[email protected]',
 'job_title': 'Agricultural engineer',
 'name': 'Jerry Moore',
 'phone_number': '720-966-9563x647'}
2023-12-20 14:53:29,853 [INFO]: Write Time: 0.019405603408813477 seconds
2023-12-20 14:53:29,857 [INFO]: Read Time: 0.0035288333892822266 seconds
2023-12-20 14:53:30,860 [INFO]: {'_id': 'v20YOK2p',
 'address': '8954 Bennett Parks Apt. 072\nSouth Andrew, MN 24485',
 'company': 'Lloyd-Gonzalez',
 'email': '[email protected]',
 'job_title': 'Radiographer, diagnostic',
 'name': 'Jimmy Anthony',
 'phone_number': '276-690-9151x204'}
2023-12-20 14:53:30,879 [INFO]: Write Time: 0.018809795379638672 seconds
2023-12-20 14:53:30,882 [INFO]: Read Time: 0.0034024715423583984 seconds

Ma foi, le script écrit et lit, il fait bien ce qu’on lui demande, le master est détecté, tout va bien !

Anakin, it’s working

Maintenant on va tester la haute disponibilité, je vais éteindre le master pour voir ce qui se passe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2023-12-20 14:55:06,232 [INFO]: Write Time: 0.021414518356323242 seconds
2023-12-20 14:55:06,236 [INFO]: Read Time: 0.0033016204833984375 seconds
2023-12-20 14:55:07,238 [INFO]: {'_id': 'c5RkmghF',
 'address': '659 Ryan Brooks\nPort Amandabury, MP 86018',
 'company': 'Taylor and Sons',
 'email': '[email protected]',
 'job_title': 'Adult guidance worker',
 'name': 'Courtney Davis',
 'phone_number': '001-681-219-7558x114'}
2023-12-20 14:55:12,242 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 6583004bdcbb6b59b92c408e, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('10.2.0.20', 27017) server_type: RSSecondary, rtt: 0.0011472350000758524>, <ServerDescription ('10.2.0.21', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('10.2.0.22', 27017) server_type: RSSecondary, rtt: 0.000923699000054512>]>
2023-12-20 14:55:12,242 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 14:55:22,251 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 6583004bdcbb6b59b92c408e, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('10.2.0.20', 27017) server_type: RSSecondary, rtt: 0.0010704152000471368>, <ServerDescription ('10.2.0.21', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('10.2.0.22', 27017) server_type: RSSecondary, rtt: 0.0010525013200413016>]>
2023-12-20 14:55:22,251 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 14:55:29,030 [INFO]: Write Time: 21.79065227508545 seconds
2023-12-20 14:55:29,034 [INFO]: Read Time: 0.004033803939819336 seconds
2023-12-20 14:55:30,037 [INFO]: {'_id': 'Pv3pF6Rp',
 'address': '1227 Wong Fort Apt. 192\nLake Jessica, AZ 96629',
 'company': 'Melendez-Ryan',
 'email': '[email protected]',
 'job_title': 'Insurance underwriter',
 'name': 'Christy Hernandez',
 'phone_number': '247.358.9687'}
2023-12-20 14:55:31,030 [INFO]: Write Time: 0.9934239387512207 seconds
2023-12-20 14:55:31,035 [INFO]: Read Time: 0.0043523311614990234 seconds
2023-12-20 14:55:32,038 [INFO]: {'_id': 'hOeYkK9C',
 'address': '0128 Tyler Walks Suite 487\nSouth Jamie, ND 28440',
 'company': 'Gonzales-Beck',
 'email': '[email protected]',
 'job_title': 'Sports coach',
 'name': 'Angela Smith',
 'phone_number': '6474403134'}

Côté script python, il fait un peu la gueule, mais il repart (donc mon cluster a refait une élection avec les deux nœuds restants).

Le script a les adaptations suffisantes pour patienter et relancer la lecture/écriture. Ce n’est pas magique, sans le retry dans le code, j’aurais eu un crash sale de mon conteneur.

Now this is pod racing!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  members: [
    {
      _id: 0,
      name: '10.2.0.21:27017',
      health: 0,
      state: 8,
      stateStr: '(not reachable/healthy)',
      uptime: 0,
[...]
    },
    {
      _id: 1,
      name: '10.2.0.22:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 1127,
[...]
    },
    {
      _id: 2,
      name: '10.2.0.20:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
[...]
    }

L’ancien master est passé en uptime 0 (normal la machine est éteinte), et son statut est passé en (not reachable/healthy). Le cluster provoque donc une élection avec les deux nœuds restants, et le cluster repart en statut OK.

Maintenant, je vais éteindre le second master, pour voir ce qui se passe :)

Le script n’écrit plus et ne lit plus, mais l’erreur est attrapée, donc il patiente tranquillement.

Star Wars, combat contre Dark Maul, Qui Gon Jinn patiente que les boucliers se baissent pour combattre

1
2
3
4
5
2023-12-20 15:04:07,518 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 15:04:17,524 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658301f4dcbb6b59b92c4212, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('10.2.0.20', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('10.2.0.20:27017: timed out')>, <ServerDescription ('10.2.0.21', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('10.2.0.21:27017: timed out')>, <ServerDescription ('10.2.0.22', 27017) server_type: RSSecondary, rtt: 0.001424114029770318>]>
2023-12-20 15:04:17,524 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 15:04:27,531 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658301f4dcbb6b59b92c4212, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('10.2.0.20', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('10.2.0.20:27017: timed out')>, <ServerDescription ('10.2.0.21', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('10.2.0.21:27017: timed out')>, <ServerDescription ('10.2.0.22', 27017) server_type: RSSecondary, rtt: 0.0014300578237990713>]>
2023-12-20 15:04:27,532 [ERROR]: insert_one operation failed. Retrying in 5 seconds...

En fait, tant que le cluster n’a pas réélu un nouveau master, le script va continuer à patienter. Donc, je vais rallumer mes deux nœuds, on va voir ce qui se passe.

Darth Sidious, yes my master

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
2023-12-20 15:05:57,626 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 15:06:07,639 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658301f4dcbb6b59b92c4212, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('10.2.0.20', 27017) server_type: RSOther, rtt: 0.0012167849999968894>, <ServerDescription ('10.2.0.21', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('10.2.0.21:27017: timed out')>, <ServerDescription ('10.2.0.22', 27017) server_type: RSSecondary, rtt: 0.001658663152246884>]>
2023-12-20 15:06:07,639 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-20 15:06:12,793 [INFO]: Write Time: 240.39230608940125 seconds
2023-12-20 15:06:12,799 [INFO]: Read Time: 0.006373882293701172 seconds
2023-12-20 15:06:13,803 [INFO]: {'_id': 'A45h689Y',
 'address': 'PSC 7749, Box 8584\nAPO AP 27201',
 'company': 'Brooks-Harris',
 'email': '[email protected]',
 'job_title': 'Archaeologist',
 'name': 'Christina Soto',
 'phone_number': '420.250.2569x307'}
2023-12-20 15:06:13,822 [INFO]: Write Time: 0.01894855499267578 seconds
2023-12-20 15:06:13,826 [INFO]: Read Time: 0.0036661624908447266 seconds
2023-12-20 15:06:14,829 [INFO]: {'_id': 'sfUmsUCB',
 'address': '7295 Emily Drive Suite 813\nWatsonport, PA 49914',
 'company': 'Lewis LLC',
 'email': '[email protected]',
 'job_title': 'Merchant navy officer',
 'name': 'Scott Flowers',
 'phone_number': '875-896-4746'}

Pendant un petit moment, on a un message de problème de synchronisation et le cluster repart avec un nouveau master qui n’est pas celui d’origine.

Homme en costume noir qui tient une lanière en cuir de manière explicite

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    {
      _id: 0,
      name: '10.2.0.21:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 24,
[...]
    },
    {
      _id: 1,
      name: '10.2.0.22:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 1693,
      syncSourceId: -1,
      infoMessage: 'Could not find member to sync from',
[...]
    },
    {
      _id: 2,
      name: '10.2.0.20:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 33,
    }
[...]
  ],

Conclusion sur les compute + containers

Donc ça fonctionne comme un cluster classique en compute mais en bien, bien plus chiant. Télérama 2/10, ne recommande pas, pose plus de problèmes qu’il n’en résout.

Je peux vous dire que pour avoir un cluster qui fonctionne normalement, j’ai explosé mon temps de setup et j’ai dû trouver des solutions de contournement largement complexifiées par les containers. Pour le moment, la plus-value n’est pas certaine comparée à un cluster construit à la main. Je ne sais pas si l’aventure vaut les ennuis :) Je suis déjà rincé sur le setup de mongo, et c’est déjà un sacré challenge de setuper un cluster en compute. J’imagine même pas à quel point en prod c’est l’éclate totale.

Temps consommé, environ 2 semaines (perlé sur mes heures de temps libre après le boulot, mes weekends) Aberrant et franchement pas marrant, j’estime que en compute en dur, en respectant les bonnes pratiques et avec le même niveau de départ, 2/3 jours c’est possible. Des fois je me demande pourquoi je m’inflige ça !

PewDiePie, why should I do it?

Oui, mais t’avais promis du kube

Vas savoir, peut-être que j’ai menti ? Ou alors que j’ai cramé toute mon énergie sur mon stupide cluster en computes ?

Attends, j’ai une idée, je vais balancer un chart Helm que je trouve et on va voir si ça fait tout aussi bien :)

Parce que c’est ce que font les gens, balancer du Helm et au diable le reste …

Je mets ça sur un cluster autopilote et on regarde si j’arrive à en faire quelque chose !

Je fais ma meilleure recherche Google

https://letmegooglethat.com/?q=mongodb+replica+set+helm+chart

Première réponse, j’ouvre le repo GitHub

GitHub mongodb Helm Chart

1
2
This chart is deprecated and no longer maintained.
It is recommended to use the Bitnami maintained MongoDB chart which has a similar feature set.

On part sur un truc sympa j’ai l’impression …

Donc on fait un tour sur le repo de Bitnami, puisque c’est le new shiny manifest-helm-ement.

https://github.com/bitnami/charts/tree/main/bitnami/mongodb

Je monte un cluster autopilote rapidement. J’ai surtout pas envie de me faire chier après tout ça, j’essaye de faire un truc qui marche mais qui dégouline.

Un coup de tf jetable et hop le cluster.

1
2
3
4
5
6
7
8
9
resource "google_container_cluster" "primary" {
  name     = "gke-mongo-cluster"
  location = var.region
 
  network    = data.google_compute_network.vpc_mongodb.name
  subnetwork = data.google_compute_subnetwork.mongodb_subnetwork.name
 
  enable_autopilot = true
}

Un petit coup de pousse caca

bousier qui pousse une boulette

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
helm install my-release oci://registry-1.docker.io/bitnamicharts/mongodb
Pulled: registry-1.docker.io/bitnamicharts/mongodb:14.4.3
Digest: sha256:493c0d01ee7cb9e6098b7b00d074af2e0a9ffcaaae818d7c3c11f4c9fdf9c745
W1220 16:53:46.625786   67516 warnings.go:70] autopilot-default-resources-mutator:Autopilot updated Deployment default/my-release-mongodb: defaulted unspecified resources for containers [mongodb] (see http://g.co/gke/autopilot-defaults)
NAME: my-release
LAST DEPLOYED: Wed Dec 20 16:53:41 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: mongodb
CHART VERSION: 14.4.3
APP VERSION: 7.0.4

** Please be patient while the chart is being deployed **

MongoDB&reg; can be accessed on the following DNS name(s) and ports from within your cluster:

    my-release-mongodb.default.svc.cluster.local

To get the root password run:

    export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace default my-release-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 -d)

To connect to your database, create a MongoDB&reg; client container:

    kubectl run --namespace default my-release-mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:7.0.4-debian-11-r2 --command -- bash

Then, run the following command:
    mongosh admin --host "my-release-mongodb" --authenticationDatabase admin -u $MONGODB_ROOT_USER -p $MONGODB_ROOT_PASSWORD

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/my-release-mongodb 27017:27017 &
    mongosh --host 127.0.0.1 --authenticationDatabase admin -p $MONGODB_ROOT_PASSWORD

Oh merde, faut être patient et manifest-tement faut savoir lire aussi, oh j’ai pas envie !

Kaamelott, Leodagan Moi j’ai appris à lire. Ben je souhaite ça à personne.

J’aurais dû commencer par ça, parce que là j’ai une patience de -5000.

1
2
3
kubectl get pods                                         
NAME                                  READY   STATUS    RESTARTS   AGE
my-release-mongodb-79bb9f5598-f5rc6   1/1     Running   0          3m37s

Ouais super, donc j’ai un nœud unique qui n’est pas un StatefulSet, pas de cluster et pas de haute disponibilité. Bon, je vais lire un peu, mais bon, moi je pensais que Helm c’était magique et incroyable, on dirait que non…

https://docs.bitnami.com/kubernetes/infrastructure/mongodb/configuration/configure-external-access-replicaset/

1
2
3
helm show values bitnami/mongodb > values.yaml
wc -l values.yaml
2203 values.yaml

Mais non, j’ai pas envie de lire 2200 lignes de conf pour faire marcher le bouzin, c’est pas très cool ça !

livre pour enfant, 1,2,3 je boude

Moi j’ai lu ça dans la doc, bon ça aide un peu quand même.

https://docs.bitnami.com/kubernetes/infrastructure/mongodb/configuration/configure-external-access-replicaset/

1
2
3
4
5
6
7
8
  architecture=replicaset
  replicaCount=2
  externalAccess.enabled=true
  externalAccess.service.type=LoadBalancer
  externalAccess.service.port=27017
  externalAccess.autoDiscovery.enabled=true
  serviceAccount.create=true
  rbac.create=true

Donc ça veut dire que je dois me taper le fichier de conf et changer les trucs sur 2200 lignes, yes on aime !

Oui, je sais, je fais le ouin-ouin, mais quand même, j’ai rien commencé que c’est déjà sacrément chiant :)

Ça part sur un accès public, de toute façon, le chart Helm ne peut pas faire autrement, et j’ai vraiment pas envie d’y toucher outre mesure.

Kaamelott, Oui, d’accord, j’en ai rien à foutre

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
helm install mongodb oci://registry-1.docker.io/bitnamicharts/mongodb --values values.yaml
Pulled: registry-1.docker.io/bitnamicharts/mongodb:14.4.3
Digest: sha256:493c0d01ee7cb9e6098b7b00d074af2e0a9ffcaaae818d7c3c11f4c9fdf9c745
W1220 17:29:03.178945   74941 warnings.go:70] autopilot-default-resources-mutator:Autopilot updated StatefulSet default/mongodb-arbiter: defaulted unspecified resources for containers [mongodb-arbiter] (see http://g.co/gke/autopilot-defaults)
W1220 17:29:03.275173   74941 warnings.go:70] autopilot-default-resources-mutator:Autopilot updated StatefulSet default/mongodb: defaulted unspecified resources for containers [auto-discovery, mongodb] (see http://g.co/gke/autopilot-defaults)
NAME: mongodb
LAST DEPLOYED: Wed Dec 20 17:28:57 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: mongodb
CHART VERSION: 14.4.3
APP VERSION: 7.0.4

** Please be patient while the chart is being deployed **

MongoDB&reg; can be accessed on the following DNS name(s) and ports from within your cluster:

    mongodb-0.mongodb-headless.default.svc.cluster.local:27017
    mongodb-1.mongodb-headless.default.svc.cluster.local:27017

To get the root password run:

    export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace default mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 -d)

To connect to your database, create a MongoDB&reg; client container:

    kubectl run --namespace default mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:6.0.9-debian-11-r0 --command -- bash

Then, run the following command:
    mongosh admin --host "mongodb-0.mongodb-headless.default.svc.cluster.local:27017,mongodb-1.mongodb-headless.default.svc.cluster.local:27017" --authenticationDatabase admin -u $MONGODB_ROOT_USER -p $MONGODB_ROOT_PASSWORD

To connect to your database nodes from outside, you need to add both primary and secondary nodes hostnames/IPs to your Mongo client. To obtain them, follow the instructions below:

  NOTE: It may take a few minutes for the LoadBalancer IPs to be available.
        Watch the status with: 'kubectl get svc --namespace default -l "app.kubernetes.io/name=mongodb,app.kubernetes.io/instance=mongodb,app.kubernetes.io/component=mongodb,pod" -w'

    MongoDB&reg; nodes domain: You will have a different external IP for each MongoDB&reg; node. You can get the list of external IPs using the command below:

        echo "$(kubectl get svc --namespace default -l "app.kubernetes.io/name=mongodb,app.kubernetes.io/instance=mongodb,app.kubernetes.io/component=mongodb,pod" -o jsonpath='{.items[*].status.loadBalancer.ingress[0].ip}' | tr ' ' '\n')"

    MongoDB&reg; nodes port: 27017

Très bien, j’attends un peu et je balance les commandes proposées (littéralement, copier-coller, je cherche même pas à comprendre). Je vais regarder l’état de mon cluster.

Développeur qui lance sa sacoche remplie de code dans son ordinateur

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
kubectl run --namespace default mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:6.0.9-debian-11-r0 --command -- bash
Warning: autopilot-default-resources-mutator:Autopilot updated Pod default/mongodb-client: defaulted unspecified resources for containers [mongodb-client] (see http://g.co/gke/autopilot-defaults)
If you don't see a command prompt, try pressing enter.
I have no name!@mongodb-client:/$     mongosh admin --host "mongodb-0.mongodb-headless.default.svc.cluster.local:27017,mongodb-1.mongodb-headless.default.svc.cluster.local:27017" --authenticationDatabase admin -u $MONGODB_ROOT_USER -p $MONGODB_ROOT_PASSWORD

MongoshInvalidInputError: [COMMON-10001] Invalid connection information: Password specified but no username provided (did you mean '--port' instead of '-p'?)
I have no name!@mongodb-client:/$ 
I have no name!@mongodb-client:/$ rs.status()
> 
> ^C
I have no name!@mongodb-client:/$ mongosh admin --host "mongodb-0.mongodb-headless.default.svc.cluster.local:27017,mongodb-1.mongodb-headless.default.svc.cluster.local:27017" --authenticationDatabase admin -u $MONGODB_ROOT_USER -p $MONGODB_ROOT_PASSWORD
MongoshInvalidInputError: [COMMON-10001] Invalid connection information: Password specified but no username provided (did you mean '--port' instead of '-p'?)
I have no name!@mongodb-client:/$ mongosh admin --host "mongodb-0.mongodb-headless.default.svc.cluster.local:27017,mongodb-1.mongodb-headless.default.svc.cluster.local:27017" --authenticationDatabase admin -u root -p VVmEMsB1g7            

[...]
rs0 [primary] admin> rs.status()
{
  members: [
    {
      _id: 0,
      name: '34.38.174.34:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 2293,
[..]
    },
    {
      _id: 1,
      name: 'mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017',
      health: 1,
      state: 7,
      stateStr: 'ARBITER',
      uptime: 2289,
[..]
    },
    {
      _id: 2,
      name: '34.77.139.40:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 2036,
[..]
    }
  ],

Kaamelott, Tarvernier; ah non, mais je touche pas à ça moi

Ça ressemble bien plus à ce que j’ai besoin, ils ont mis deux replicas et un arbiter (ce qui est sur le papier un peu mieux que moi). Le truc, c’est que mes nœuds sont exposés publiquement et avec des disques minuscules (que je peux changer dans le Helm ensuite, je sais).

Je balance mon script sur le cluster dans Kube

Je vais connecter mon script dessus, je m’attends à pas trop de miracle avec l’accès public, mais on va voir ce qu’il peut nous faire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2023-12-21 10:19:19,582 [INFO]: {'_id': 'kk6StBOK',
 'address': '39767 Lewis Parkways\nLake Marcshire, TN 74537',
 'company': 'Rhodes LLC',
 'email': '[email protected]',
 'job_title': 'Control and instrumentation engineer',
 'name': 'Charlotte Coleman',
 'phone_number': '(793)964-3952'}
2023-12-21 10:19:19,775 [INFO]: Write Time: 0.1933143138885498 seconds
2023-12-21 10:19:19,794 [INFO]: Read Time: 0.018149852752685547 seconds
[...]
2023-12-21 10:20:04,900 [ERROR]: 34.38.174.34:27017: timed out
2023-12-21 10:20:04,900 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:20:14,949 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658411408203ae64f97cbec6, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('34.38.174.34:27017: timed out')>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.007880099609598505>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
[...]
2023-12-21 10:21:05,050 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:21:15,129 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658411408203ae64f97cbec6, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None, error=NetworkTimeout('34.38.174.34:27017: timed out')>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.017201691499005703>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
2023-12-21 10:21:15,129 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:21:21,832 [INFO]: Write Time: 96.95796489715576 seconds
2023-12-21 10:21:21,841 [INFO]: Read Time: 0.0086212158203125 seconds
2023-12-21 10:21:22,844 [INFO]: {'_id': 'dx8AhU9t',
 'address': '6099 Wong Rest Apt. 513\nAmandashire, UT 37709',
 'company': 'Mann Inc',
 'email': '[email protected]',
 'job_title': 'Product manager',
 'name': 'Christine Harrison',
 'phone_number': '5805307777'}
[...]
2023-12-21 10:21:55,443 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658411be8203ae64f97cbedb, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.0018316649999974288>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
2023-12-21 10:21:55,443 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:22:05,520 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658411be8203ae64f97cbedb, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.0018316649999974288>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
[...]
2023-12-21 10:23:20,931 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:23:27,378 [INFO]: Write Time: 96.99217867851257 seconds
2023-12-21 10:23:27,384 [INFO]: Read Time: 0.006409406661987305 seconds
2023-12-21 10:23:28,387 [INFO]: {'_id': 'RC3tgofx',
 'address': '850 Taylor Hill\nKristenberg, OR 51865',
 'company': 'Bradley, Campos and Archer',
 'email': '[email protected]',
 'job_title': 'Lobbyist',
 'name': 'Martha Sparks',
 'phone_number': '413-912-0603x6659'}

[...]

2023-12-21 10:24:02,507 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 6584123d8203ae64f97cbef0, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.008535056000027907>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
2023-12-21 10:24:02,507 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:24:12,570 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 6584123d8203ae64f97cbef0, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription [...]
2023-12-21 10:25:27,997 [ERROR]: insert_one operation failed. Retrying in 5 seconds...
2023-12-21 10:25:33,871 [INFO]: Write Time: 96.42597675323486 seconds
2023-12-21 10:25:33,943 [INFO]: Read Time: 0.07238554954528809 seconds
2023-12-21 10:25:34,946 [INFO]: {'_id': '1uE7QVGI',
 'address': '74749 Garcia Rapids\nSullivanberg, MH 07131',
 'company': 'Jones, Reid and Romero',
 'email': '[email protected]',
 'job_title': 'Barrister',
 'name': 'Matthew Rhodes',
 'phone_number': '256-831-7892x399'}
[...]
2023-12-21 10:26:05,866 [ERROR]: No replica set members match selector "Primary()", Timeout: 5.0s, Topology Description: <TopologyDescription id: 658412b88203ae64f97cbf05, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('34.38.174.34', 27017) server_type: Unknown, rtt: None>, <ServerDescription ('34.77.139.40', 27017) server_type: RSSecondary, rtt: 0.0020834800000102405>, <ServerDescription ('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local', 27017) server_type: Unknown, rtt: None, error=AutoReconnect('mongodb-arbiter-0.mongodb-arbiter-headless.default.svc.cluster.local:27017: [Errno -2] Name or service not known')>]>
2023-12-21 10:26:05,866 [ERROR]: insert_one operation failed. Retrying in 5 seconds...

Et bien, c’est pas glorieux tout ça. Je n’ai même pas essayé de casser un pod ou de faire la moindre perturbation dans Kube.

J’ai tronqué les logs, mais en 7 minutes, j’ai déjà perdu 4 fois mon intégrité de cluster. J’ai une latence supérieure, mais pas vraiment significative. Donc en termes de vitesse de setup, Helm est incontestablement imbattable, mais en stabilité pure et en troubleshooting, il faut un froc sacrément solide pour s’en sortir.

En termes de complexité technologique également, c’est bien trop overkill :

  • un cluster Kube
  • des ressources en pagaille
  • des load balancers

Kaamelott: Leodagan, c’est pas si simple.

Pour rigoler, j’ai lancé une upgrade de cluster Kube, le cluster a planté pendant cette période, mais pas facile de savoir si c’est une coïncidence ou l’upgrade qui a fait tomber mon cluster.

C’est possible qu’il n’aime pas tellement communiquer avec les IP publiques puisqu’il me fait régulièrement des timeouts. C’est un peu con d’utiliser les adresses publiques pour l’intégrité du cluster, mais bon, c’est pas moi, c’est Bitnami haha.

Aussi, un problème d’exposer publiquement le cluster et de ne pas proposer d’Internal Load Balancer pour éviter de retrouver son cluster Mongo sur Shodan.

Non clairement, je m’arrête là avec les bases de données sur Kube !

Conclusion: comme d’hab, j’aime rien, mais vous commencez à avoir l’habitude

En substance, on pourrait résumer mon barguignage comme ceci :

  • Moi, j’aime pas les computes
  • Moi, j’aime pas Helm
  • Moi, j’aime pas les bases de données accessibles publiquement
  • Moi, j’aime pas les fichiers de conf
  • Ouin ouin, les bases de données en cluster c’est compliqué.

Maxi Schtroumpf grognon

Le Schtroumpf grognon

Mais comme c’est bientôt Noël, je vais vous gratifier d’une meilleure conclusion.

Si vraiment vous voulez faire un cluster Mongo, je pense que la meilleure option, c’est encore le compute en dur. C’est vraiment bête à dire comme ça, mais Kubernetes n’apporte pas de magie et les containers apportent de la complexité supplémentaire dans ce cas.

Pour les opérations manuelles que vous allez devoir effectuer de toutes façons :

  • les montées de versions de Mongo
  • les upgrades de libs
  • les resize des disques
  • les actions en ligne de commande Mongosh

Ça reste la solution la plus stable et la plus confortable pour ce besoin. Alors oui, c’est plus long à reconstruire, mais ça n’empêche pas de construire les machines en Terraform, maintenir et faire évoluer un Ansible, activer le service de mises à jour automatiques (en hold sur le package Mongo, pour gérer manuellement les changements de version même mineurs).

Le compute + container peut être une solution quand on ne veut pas mettre du Kube dans son infra pour des questions de coût, de compétences, ou parce que la charge ne rentre pas dans Cloud Run. J’en ai pas vraiment parlé, mais si on utilise des instances groups managées, c’est bien plus simple et souvent plus rapide de builder des containers plutôt que faire des images avec Packer. Et quitte à faire du compute, quand c’est possible et pas trop consommateur de temps, autant utiliser des containers avec l’image COS de Google.

  • On réduit la surface d’attaque
  • L’OS est durci et les mises à jour automatiques
  • On gère les mises à jour du code et des dépendances plus simplement dans un container.

En ce qui concerne Helm, on peut sûrement faire des trucs super avec, mais une inquiétude majeure que j’ai avec cet outil, c’est qu’on maîtrise rien. Une base de données non maîtrisée et instable, c’est pas possible. Alors oui, j’aurais pu passer plus de temps et faire un cluster par moi-même en Kube, mais mon intuition c’est que ça prendrait un temps fou avec un résultat qui n’est pas au rendez-vous.

That’s all folks