Vue normale

Reçu aujourd’hui — 30 mai 2025

Plusieurs façons de gérer son code et ses environnements avec Terraform

30 mai 2025 à 14:17

Au fil des années, et depuis maintenant pratiquement six ans que je pratique terraform, j’ai pu voir et écrire une quantité non négligeable de lignes de code terraform, mes propres méthodes et préférences ayant évolué avec le temps. Je vous propose donc un petit florilège de ce qu’on peut faire avec cet outil merveilleux.

Ai-je encore besoin de présenter cet incontournable du domaine qu’on appelle l’infrastructure-as-code ? L’outil d’Hashicorp est devenu une référence en quelques années dans le domaine de la gestion d’infrastructure dans les nuages. Son modèle à base d’extensions, les « providers », la capacité de reposer sur des modules de codes pré-établis, ont conquis bon nombre d’équipes et d’indépendants, et de bricoleurs du dimanche comme moi. Le fait que l’outil soit open-source est évidemment un autre critère à ne pas oublier (alors oui, bon, entre temps, ça s’est un peu compliqué…). Il ne faut par contre pas croire ceux qui vous sortent encore qu’un seul code peut permettre de déployer chez n’importe quel fournisseur, c’est juste de la grosse connerie. Mais les concepts d’utilisation et le langage restent eux bien identiques, et surtout, permettent dans la même exécution de déployer chez plusieurs fournisseurs en même temps; moi par exemple, quand je déploie une nouvelle machine sur mon serveur Proxmox, je peux déclarer une entrée DNS chez OVH dans le même temps, le tout avec une seule exécution de terraform. C’est ce genre de puissance qui fait que l’outil est apprécié (on peut même commander ses pizzas avec :P).

Un autre aspect est de pouvoir reproduire pratiquement à l’identique et de manière illimitée un même environnement. La souplesse dans l’utilisation du langage est un autre critère qui a fait son succès, et j’ai pu le comprendre en partie face aux différents « codes » que j’ai pu croiser, et certains modèles de code permettant notamment de gérer plusieurs environnements m’ont vraiment plu dans leur philosophie. C’est ce que je compte vous partager aujourd’hui. D’une manière assez rigolote : on va y aller par étape dans la complexité.

1° niveau : tout hardcodé

C’est ce qu’on voit dans les documentations des « providers », et c’est généralement la première étape de ce qu’on produit comme code quand on débute. Je vais volontairement éluder l’organisation des fichiers pour me concentrer sur le code lui-même : on peut tout coller dans un seul fichier, ou dans plusieurs, et leur nommage importe peu tant que leur extension est .tf.

resource "ovh_domain_zone_record" "vps_ipv4" {
    zone = "domain.tld"
    subdomain = "mysub"
    fieldtype = "A"
    ttl = "60"
    target = "1.2.3.4"
}

Simple, efficace, mais absolument pas souple quand il s’agira de reproduire ça pour d’autres enregistrements. La méthode simple consistera à copier/coller ce code pour créer une deuxième ressource, une troisième, etc. Ça devient vite un enfer, et en plus, tout est dans le même code donc le même fichier d’état dont on reparlera assez vite. C’est là que les variables entrent en jeu.

2° niveau : variables, variables everywhere (et des maps)

Certes ce n’est pas ce que je recommande, mais une des premières utilisations des variables que j’ai faites à été de créer une variable par propriété que je souhaitais pouvoir moduler/réutiliser. Ces variables sont à enregistrer dans un fichier .tfvars, on comprend vite la signification de l’extension du fichier. Sans beaucoup plus d’explications parce que l’exemple sera suffisamment parlant, voilà le résultat :

resource "ovh_domain_zone_record" "vps_ipv4" {
    zone = var.domain
    subdomain = var.subdomain
    fieldtype = "A"
    ttl = "60"
    target = var.ip_address
}

Ma première évolution de ça a été de dupliquer le code comme au premier niveau, et donc avec les variables. En clair, j’avais un jeu de variables pour chaque domaine que je souhaite ajouter. C’est lourdingue, surtout quand on sait qu’on doit déclarer chaque variable qu’on compte utiliser avant de lui affecter une valeur. Aussi, le jour où une évolution majeure demande de réécrire le code (parce qu’on aura voulu renommer la propriété target par exemple), ça demandera de faire x fois la même modification. On reste également dans une gestion unique de l’état.

Une première manière de réduire un peu le poids de ces déclarations de variables a été l’utilisation de listes et de maps. Plutôt que d’avoir une variable par propriété d’une même ressource, on crée une variable par ressource, et cette variable est en l’occurrence une map contenant toutes les propriétés dont on a besoin. Le résultat est parlant là encore :

resource "ovh_domain_zone_record" "vps_ipv4" {
    zone = var.domain["domain_name"]
    subdomain = var.domain[subdomain"]
    fieldtype = "A"
    ttl = "60"
    target = var.domain["ip_address"]
}

variable "domain" {}

domain = {
  "domain_name" = "domain.tld"
  "subdomain"   = "mysub"
  "ip_address"  = "1.2.3.4"
}

J’ai inclus un exemple de variable en dessous pour qu’on comprenne la différence avec le cas précédent. Si on commence à modulariser ce code, l’utilisation d’une map comme celle-ci permet de grandement limiter la duplication de code, même si pour l’instant, nous n’avons pas encore évolué sur notre gestion de l’état.

Un très bonne évolution amenée par Hashicorp a été l’utilisation de boucles, via la fonction for_each. Elle permet de boucler sur une map ou une liste pour répéter un même code à partir du contenu de celle-ci. Ajoutez le fait que cette map peut être indexée, vous commencerez peut-être à comprendre où je veux en venir. On peut dès lors créer la liste des ressources dans une seule variable avec une série de propriétés. Ici, il « suffit » de faire la liste des domaines et des IP associées dans la variable domain, et on peut boucler dessus:

resource "ovh_domain_zone_record" "vps_ipv4" {
  for_each = var.domains
  zone = each.value.domain_name
  subdomain = each.value.subdomain
  fieldtype = "A"
  ttl = "60"
  target = each.value.ip_address
}

variable "domains" {
  type = map(any)
} 

domains = {
  record1 = { 
  domain_name = "domain.tld",
  subdomain = "mysub",
  ip_address = "1.2.3.4"},
  record2 = { 
  domain_name = "domain2.tld",
  subdomain = "mysub2",
  ip_address = "4.5.6.7"}
}

Et depuis, ce for_each a été étendu pour être utilisé non seulement sur les ressources, mais aussi carrément sur les modules eux-mêmes. Ça donne quelque chose de particulièrement puissant et qui peut rester assez léger à lire. Swag.

3° niveau : le couple fichier tfvars + changer la clé de son backend

Eh oui, il est temps de parler d’un élément que je un peu trop vite éludé pour l’instant. Le fonctionnement de terraform repose sur l’enregistrement de l’état de l’infrastructure décrite dans le code, une fois celui-ci appliqué. Lors des exécutions successives, l’outil compare le code, l’état, et ce qui est présent côté plateforme cible pour ensuite indiquer si des changements sont à effectuer (ajout, suppression, modification). Le résultat est donc stocké dans cet état, le fameux « tfstate ».

Quand on ne déclare rien, terraform va stocker son état dans un fichier local, appelé terraform.tfstate. Mais on peut configurer un backend distant pour le stockage de ce fichier, qui est d’ailleurs nécessaire dès lors que le code est partagé via un outil type git, pour que chaque intervenant susceptible de travailler avec le code puisse exploiter un état commun. C’est là qu’on commence à rigoler, mais aussi à faire une chose lourde et rétrospectivement assez dangereuse : associer une clé, c’est-à-dire le chemin du fichier dans le stockage, et un fichier de variables. Pourquoi ? parce qu’on ne peut pas variabiliser une clé de backend comme on le ferait pour le reste de la configuration des providers. Mais bon, on peut enfin séparer les états selon des critères (généralement, la cible d’exécution : sandbox, production…). Mais plantez-vous une fois, et appliquez le mauvais fichier sur l’environnement cible, et c’est potentiellement tout une production qui tombe. C’est clairement pas ce que je recommande, mais j’ai eu à gérer à un moment donné un backend qui ne supportait pas l’utilisation des Workspaces, et c’était à l’époque la seule solution qui nous était apparu comme acceptable pour éviter trop de duplications de code, du genre un dossier par environnement, chacun avec sa conf, son fichier de variables, son backend. Le jour où on doit modifier quelque chose, il faut le répliquer ensuite dans chaque dossier. Pas toujours foufou quand on ne peut pas exploiter les modules.

4° Niveau: les workspaces !

Ça a été une fonctionnalité qui, si le backend le supporte, permet de réduire un peu la lourdeur de la gestion de l’état. En effet, vous déclarez une clé de backend « de base » dans votre configuration, et chaque workspace créé va générer une clé de backend à partir de la base, mais avec un suffixe qui est le nom du workspace. Pourquoi je dit « si le backend le supporte » ? Parce que le caractère séparateur est un « : » qui n’est pas forcément bien toléré par la technologie de stockage. On reste malgré tout dans le domaine du « faut faire gaffe au fichier de conf », mais l’avantage c’est qu’on a « juste » à gérer un tfvars. Je vous laisse lire l’excellente page de Stéphane Robert sur le sujet pour en comprendre l’intérêt.

5° Niveau: Le boss final de l’élégance

Vraiment, c’est ce que j’ai eu l’occasion de voir le plus élégant, tout en étant pas évidemment la solution absolue. Seulement, c’est particulièrement pratique si on doit répliquer sur plusieurs environnements. On reprend une grande partie des éléments précédents, à savoir les workspaces, les maps pour les variables. Mais exit les tfvars, tout repose sur le workspace et les valeurs « par défaut » des variables.

Je ne vais pas reprendre l’exemple du domaine parce que ça n’aurait que peu de sens. On va cette fois prendre comme exemple la définition d’un subnet dans un projet Hetzner Cloud pour trois environnements différents:

resource "hcloud_network" "privNet" {
  name     = terraform.workspace
  ip_range = var.subnets[terraform.workspace].subnet
}

variable "subnets" {
  description = "List of subnets for all envs"
  type = map(any)
  default = {
    dev = {
      subnet = "10.1.0.0/24"
    },
    preprod = {
      subnet = "10.2.0.0/24"
    },
    prod = {
      subnet = "10.3.0.0/24"
    }
  }
}

Ensuite, il n’y a plus qu’à initialiser son environnement, son workspace donc, et d’appliquer le code, et il va sélectionner le subnet en fonction du workspace. D’ailleurs, il est possible aussi de créer dans la map une sous-liste si on veut créer plusieurs subnets, et d’appliquer un for_each. Ça pourrait alors donner un truc dans le genre:

resource "hcloud_network" "privNet" {
  for_each = toset(var.subnets[terraform.workspace].subnets)
  name     = "${terraform.workspace}-${index(var.subnets[terraform.workspace].subnets, each.value) +1}"
  ip_range = each.value
}

variable "subnets" {
  description = "List of subnets for all envs"
  type = map(any)
  default = {
    dev = {
      subnets = [ "10.1.0.0/24", "10.2.0.0/24" ]
    },
    preprod = {
      subnet = [ "10.3.0.0/24", "10.4.0.0/24" ]
    },
    prod = {
      subnet =  [ "10.5.0.0/24", "10.6.0.0/24" ]
    }
  }
}

Bon là je me complique la vie parce qu’avec une simple liste sans « label », faut ruser pour trouver un nom pour chaque subnet côté Hetzner, mais vous avez l’idée. Testé rapidement, ça donne ça comme plan :

tofu plan

OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

OpenTofu will perform the following actions:

  # hcloud_network.privNet["10.5.0.0/24"] will be created
  + resource "hcloud_network" "privNet" {
      + delete_protection        = false
      + expose_routes_to_vswitch = false
      + id                       = (known after apply)
      + ip_range                 = "10.5.0.0/24"
      + name                     = "prod-1"
    }

  # hcloud_network.privNet["10.6.0.0/24"] will be created
  + resource "hcloud_network" "privNet" {
      + delete_protection        = false
      + expose_routes_to_vswitch = false
      + id                       = (known after apply)
      + ip_range                 = "10.6.0.0/24"
      + name                     = "prod-2"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Vous pouvez donc dans un seul fichier de variables, définir l’intégralité de vos environnements. Et si pour une quelconque raison certaines ressources n’ont pas à être déployées dans tous les environnements, vous pouvez conditionner leur application :

resource "hcloud_network" "privNet" {
  count = try(terraform.workspace == "prod") ? 1 : 0
  name = terraform.workspace
  ip_range = var.subnets[terraform.workspace].subnet
}

Ici, on ne créera le subnet que si l’on est « en prod ».

Et tant d’autres subtilités

Il y a certainement plein d’autres méthodes pour gérer son code terraform, je n’ai ici recensées que celles que j’ai pu voir en cinq ans de pratique (ça fait un an que j’en avais pas touché, ça fait bizarre et c’est toujours aussi rigolo à bricoler). Je suis d’ailleurs curieux de voir un peu comment vous procédez. D’ailleurs, si on va voir du côté de chez Julien Hommet on peut voir que la définition de la map va plus loin que mon simple « any » de gros feignant que je suis, je recommande évidemment sa syntaxe, plus prédictive. J’imagine qu’il y a d’autres aspects que je n’ai pas abordé ici et que vous pourriez certainement porter à ma connaissance (et à ceux qui lisent l’article du coup) 🙂

Reçu avant avant-hier

Récuperer les bases de données de feu mon cluster k3s, sans backup

22 mai 2025 à 19:23

Oui, comme la maxime qui parle d’un cordonnier, je n’avais pas encore mis en place les backups MariaDB quand mon dernier raspberry Pi, plus précisément sa carte SD, a rendu l’âme. Donc pour la restauration sur la nouvelle installation, c’est compliqué. Mais pas impossible non plus 🙂

Pour ceux qui n’ont pas forcément suivi toute l’histoire de ce cluster, petit rappel: deux Raspberry Pi, du k3s sur Raspberry Pi OS, un master, un worker. J’avais retiré le worker pour bosser sur une refonte, à savoir une installation de Talos sur SSD, mais le master qui s’est retrouvé tout seul a commencé à montrer des signes de fatigue. J’avais juste mis ça sur la charge de porter tous les pods, mais il s’avère que les écritures plus nombreuses ont accéléré la mort de la carte SD.

Oui car dans une installation Kubernetes, et même si les principaux volumes de mes applications sont provisionnés/montés par le driver CSI NFS, les logs, et toute l’activité liée aux pods, sont écrits sur le volume local de la machine, et donc ici une carte SD. même en ayant pris un modèle plus adapté à ce cas d’usage, au bout de plus de 5 ans, ça commençait à faire trop, et elle m’a lâché sans prévenir pendant mes vacances.

Faites des sauvegardes bordel !

Ou alors « Faites ce que je dis, pas ce que je fais ». Après la réinstallation de Mariadb via le mariadb-operator (qu’on avait d’ailleurs fait en live, abonne-toi toussa :D), j’avais laissé de côté les sauvegardes pour me concentrer sur la migration des bases et la reconfiguration des applications. J’ai ensuite porté mon attention ailleurs et je ne suis jamais revenu dessus. La seule forme de sauvegarde que j’ai, c’est que le datadir de l’instance MariaDB n’est pas sur les nœuds mais sur mon NAS, et j’ai un job de sauvegarde quotidien vers une Storage Box Hetzner. Même sans cette externalisation, le fait que le datadir est toujours disponible est déjà une certaine sécurité.

Sauf que… qui dit mariadb-operator dit gestion par l’opérateur du compte root de l’instance, donc quand il s’agit de restaurer, je ne peux pas juste resynchroniser le datadir dans le nouveau volume, parce que je n’ai plus les manifestes qui vont avec, en clair, je n’ai plus le compte root. J’ai donc un joli datadir, mais pas directement exploitable pour la restauration. Sans parler que je profite de la nouvelle installation pour passer sur une version plus récente de MariaDB. bref, ça me complique la vie. Mais ne c’est pas perdu pour autant.

La tactique de la lessive

(si vous l’avez celle-là…) J’avais déjà eu à gérer ça sur le serveur d’un client un jour, je me souviens plus la cause de départ, mais on avait besoin de réinitialiser le compte root de son instance MySQL, en gros, on s’était enfermé dehors. L’astuce, c’est de démarrer une instance MySQL en spécifiant le datadir, et en utilisant l’option --skip-grant-tables pour démarrer sans authentification. À proscrire absolument dans le monde réel, mais dans le besoin…

Dans mon cas, j’ai bossé sur mon installation WSL2 sur Ubuntu 22.04. J’ai commencé par vérifier la version de MariaDB que j’avais utilisé pour le déploiement de l’instance: 10.11.4. J’ai suivi ensuite la doc officielle pour ajouter le dépot mariadb pour ma branche:

curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version="mariadb-10.11"

Comme souvent dans le cas de cette méthode horrible du curl | bash, j’ai vérifié le script avant, vérification que je vous recommande de faire systématiquement, ici tout va bien. Une fois donc le script exécuté, qui aura ajouté la clé GPG et le dépot. On peut donc passer à l’installation:

$ sudo apt install mariadb-server
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
htop libnl-3-200 libnl-genl-3-200 libsass1 libva-wayland2 linux-headers-5.15.0-139 linux-headers-5.15.0-139-generic
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
galera-4 libcgi-fast-perl libcgi-pm-perl libconfig-inifiles-perl libdbd-mysql-perl libdbi-perl libfcgi-bin libfcgi-perl libfcgi0ldbl libhtml-template-perl libmariadb3 libmysqlclient21
libterm-readkey-perl liburing2 libwrap0 mariadb-client mariadb-client-core mariadb-common mariadb-server-core mysql-common pv socat
Suggested packages:
libmldbm-perl libnet-daemon-perl libsql-statement-perl libipc-sharedcache-perl mailx mariadb-test doc-base
The following NEW packages will be installed:
galera-4 libcgi-fast-perl libcgi-pm-perl libconfig-inifiles-perl libdbd-mysql-perl libdbi-perl libfcgi-bin libfcgi-perl libfcgi0ldbl libhtml-template-perl libmariadb3 libmysqlclient21
libterm-readkey-perl liburing2 libwrap0 mariadb-client mariadb-client-core mariadb-common mariadb-server mariadb-server-core mysql-common pv socat
0 upgraded, 23 newly installed, 0 to remove and 3 not upgraded.
Need to get 28.2 MB of archives.
After this operation, 159 MB of additional disk space will be used.
Do you want to continue? [Y/n]

Première chose que je fais, c’est de désactiver le service, car il est de coutume dans les environnement « Debian-like » que l’installation d’un paquet avec un service inclue l’activation et le démarrage par défaut dudit service. J’ai donc déjà un serveur MariaDB démarré à la fin de l’installation:

sudo systemctl disable --now mariadb.service

Ensuite, je récupère une copie via rsync de mon datadir dormant:

rsync -a 192.168.1.201:/volume1/Kube/pvc-3dbb41c0-41ea-4f09-90cc-4aa186124266/ ./datadir/

Très important, ne jamais travailler sur la version d’origine, qui doit rester en l’état si tout ne se passe pas comme prévu. Ensuite, on en vient à la partie la plus importante. J’ai du retâter un peu du --help, et prendre deux/trois messages d’erreur dans la gueule, puis, enfin, le Graal :

$ mysqld --skip-grant-tables --datadir=./datadir --socket ./mysqld.sock --pid-file=./mysqld.pid
2025-05-19 21:16:17 0 [Note] Starting MariaDB 10.11.11-MariaDB-ubu2204 source revision e69f8cae1a15e15b9e4f5e0f8497e1f17bdc81a4 server_uid KCkK/FgNE+mocr6SqlkAw7a5WoI= as process 52647
2025-05-19 21:16:17 0 [Note] InnoDB: Compressed tables use zlib 1.2.11
2025-05-19 21:16:17 0 [Note] InnoDB: Number of transaction pools: 1
2025-05-19 21:16:17 0 [Note] InnoDB: Using AVX512 instructions
2025-05-19 21:16:17 0 [Note] InnoDB: Using liburing
2025-05-19 21:16:17 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB
2025-05-19 21:16:17 0 [Note] InnoDB: Completed initialization of buffer pool
2025-05-19 21:16:17 0 [Note] InnoDB: File system buffers for log disabled (block size=4096 bytes)
2025-05-19 21:16:17 0 [Note] InnoDB: End of log at LSN=3957967525
2025-05-19 21:16:17 0 [Note] InnoDB: 128 rollback segments are active.
2025-05-19 21:16:17 0 [Note] InnoDB: Removed temporary tablespace data file: "./ibtmp1"
2025-05-19 21:16:17 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...
2025-05-19 21:16:17 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.
2025-05-19 21:16:17 0 [Note] InnoDB: log sequence number 3957967525; transaction id 2439228
2025-05-19 21:16:17 0 [Note] InnoDB: Loading buffer pool(s) from /home/seboss666/git/git.seboss666.ovh/k3s_platform/kubernetes/databases/datadir/ib_buffer_pool
2025-05-19 21:16:17 0 [Note] Plugin 'FEEDBACK' is disabled.
2025-05-19 21:16:17 0 [Warning] You need to use --log-bin to make --expire-logs-days or --binlog-expire-logs-seconds work.
2025-05-19 21:16:17 0 [Note] Server socket created on IP: '127.0.0.1'.
2025-05-19 21:16:17 0 [Note] InnoDB: Buffer pool(s) load completed at 250519 21:16:17
2025-05-19 21:16:17 0 [Note] mysqld: ready for connections.
Version: '10.11.11-MariaDB-ubu2204' socket: './mysqld.sock' port: 3306 mariadb.org binary distribution

Wouhou, désormais, on peut brancher un client sur le socket qu’on a indiqué et vérifier la présence des bases :

$ mysql -S ./datadir/mysqld.sock
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.11.11-MariaDB-ubu2204 mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| freshrss           |
| gitea              |
| information_schema |
| mariadb            |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
7 rows in set (0.045 sec)

TADA !!!

On peut dès lors dumper les bases pour préparer l’importation dans la nouvelle instance, en spécifiant aussi le socket :

mysqldump -S ./datadir/mysqld.sock -Q -B --opt gitea > gitea.sql

Quelques dizaines de minutes plus tard passées sur Talos, les applis sont de nouveau disponible \o/

Prochaine étape: mettre en place des dumps et des sauvegardes « Kube »

J’ai déjà cette histoire de datadir, mais on l’a vu, en cas de restauration c’est assez contraignant et ça aurait pu être encore plus long si je n’avais pas déjà su comment procéder. Pire, on ne peut pas restaurer « en l’état » parce que les infos de l’instance déployée par l’opérateur sont perdues dans la bataille, ce qui oblige à tout réinstaller.

Il faut donc que je mette en place des dumps, l’opérateur va m’y aider, mais aussi la sauvegarde des manifestes Kubernetes et là on reparlera de Velero je pense. Les dumps, c’est une sécurité et la facilité de migration des bases, les manifestes permettent de rétablir l’instance dans l’état le plus proche de l’original, ce qui m’aurait évité les heures passées à tout refaire de zéro.

Donc oui, plus que jamais, faites des sauvegardes (et testez-les aussi, de temps en temps, c’est mieux) 🙂

Extraire des infos de votre cluster Kubernetes au format CSV

4 février 2025 à 18:45

Autre petit déterrage de brouillon. Je sais, quand on bosse sur en tant que Sysadmin, c’est pas spécialement le premier format de données auquel on pense. Mais quand on commence à bosser sur un cluster avec des dizaines de sites web déployés, plus des utilitaires/opérateurs autour, extraire les infos devient un léger sport, pire, quand il faut partager l’info à des gens moins techniques, c’est compliqué. Et donc, le format CSV peut aider, je vous explique.

Quand j’étais encore chez LinkByNet/Accenture, un des clusters Kube que j’avais à gérer était mutualisé entre plusieurs entités d’un client (appelés maisons), les applications étaient identifiées clairement comme telles via un label, mais on a découvert que cet élément d’inventaire, qui permettait la refacturation de la consommation dans le cluster à chaque maison (je vous passe le reste des détails), n’était pas toujours positionné correctement. Avant d’effectuer des corrections il est donc nécessaire de récupérer les informations existantes.

La configuration du label « maison » est splittée dans autant de dépôts git qu’on a de sites déployés, donc on s’évite le truc, et on part du principe qu’on peut récupérer l’information dans le cluster directement. Et pour traiter le résultat, surtout le partager au client, vu le format que ça risque de prendre (un tableau), on pense partager un tableau Excel pour ensuite se voir retourner la version corrigée.

Si vous manipulez régulièrement Kubernetes, vous savez que Excel, et même CSV, ne sont pas des formats supportés pour récupérer les informations, on a que JSON et YAML (en tout cas pour avoir les détails des déploiements/statefulsets/daemonsets, comme les labels), et éventuellement jsonPath pour pré-filtrer les infos. Ceci dit, à partir de JSON il y a potentiellement de quoi faire, en particulier avec jq. J’aime bien jq, même si je rote du sang à chaque fois que j’essaie de l’utiliser. Quelques recherches plus tard, mes intuitions étaient correctes et il s’avère que jq permet de formater les donner en CSV. Mais au fait, c’est quoi le CSV ?

Petit retour sur un format texte simple et pourtant si pratique

CSV ça veut dire Comma Separated Values, soit valeurs séparées par des virgules. Oui, c’est aussi simple que ça en a l’air, dans un fichier texte, la première ligne peut désigner éventuellement les titres des colonnes, et les lignes suivantes les valeurs. Un exemple ?

Nom,Prénom,Profession,Ville
Chabrier,Pierre,Pleureur,MontCuq
Lévy,Sylvain,Sniper,Saint-Denis
Christ,Jesus,,Nazareth

Vous voyez, c’est assez Simple. Et en vrai on pourrait aussi procéder de la même façon avec la première colonne de description et les suivantes de données, voire les deux. Vous avez remarqué la troisième ligne ? Pour indiquer qu’on ne « remplit » pas la colonne correspondant ici à Profession, on laisse juste un vide avec deux virgules successives. Comme je l’ai dit, simple, efficace.

Reste à savoir comment on obtient ça à partir du JSON que nous retourne kubectl.

Dans le dur

Bien, comme je l’ai dit, jq va nous servir très fort, parce qu’il a justement une fonction dédiée au formatage de données en CSV. Commençons déjà par récupérer ce dont on a besoin. On cherche donc les infos des Deployments, StatefulSets, DaemonSets de tous les namespaces. Sur le papier, j’aurais pu utiliser jsonPath comme sortie pour récupérer les champs que je voulais, mais j’ai noté de déjà passer par jq pour filtrer tout ça et ne garder que ce qui m’intéresse, à savoir le type (Kind), le Namespace, le nodeselector (oui, on a eu une histoire de nodepool à gérer aussi), et le fameux label « maison ». Avec jq j’y suis arrivé plus simplement alors qu’avec jsonPath j’ai toujours buté sur un des points (mais là j’avoue, entre le premier brouillon y’a trois ans et maintenant, j’ai oublié lequel). Bref, tout ça finit dans un fichier json, ça donne quelque chose comme ça :

kubectl get deploy,sts,ds -o json -A | jq -r '[ .items[] | {Kind: .kind , Name: .metadata.name, Maison: .metadata.labels["maison"], Namespace: .metadata.namespace, selector: .spec.template.spec.nodeSelector["lbn.fr/project"] }]' > workload.json

Pas super simple à lire, mais pas non plus super compliqué à comprendre. Maintenant vient la partie qui permet de structurer notre futur fichier CSV. On commence par les entêtes. Actuellement on a ça dans le fichier json:

[{"Kind":"Deployment", "Name":"site1-com","Maison":"maison1","Namespace":"dev-site1-com","selector","web"},
{"Kind":"Deployment", "Name":"site2-com","Maison":"maison2","Namespace":"dev-site2-com","selector","web"}]

La première action va donc être d’extraire les clés. La petite triche ici c’est qu’on est certain qu’on peut se reposer sur la première « ligne » du tableau JSON parce qu’on a exactement les mêmes champs dans toutes les entrées. Et petite subtilité par contre, comme il s’agit des clés, on doit forcer jq à ne pas les trier par ordre alphabétique sinon ça fout la grouille :

cat workload.json | jq '.[0]|keys_unsorted | @csv' > workload.csv

  • Le [0], c’est pour forcer à se baser sur la première ligne uniquement (sinon il sortirait les clés de toutes les lignes et on aurait autant de lignes avec les noms des champs qu’on a d’entrées dans le tableau JSON)
  • keys_unsorted, comme je l’ai dit, permet de ne pas trier alphabétiquement
  • le  @csv est la fonction magique qui formate le résultat pour nos besoins.
  • le chevron simple permet de s’assurer que c’est la première ligne du fichier, puisque comme ça tout contenu éventuellement existant est directement supprimé pour être remplacé par notre unique ligne.

C’est pas si compliqué hein ? Reste ensuite à extraire les valeurs, pour toutes les lignes cette fois. La logique reste grosso modo la même, à part que là, et j’ignore pourquoi, pas besoin de forcer à ne pas trier les valeurs, elles restent dans le bon ordre :

cat workload.json | jq '.[]|map(values) | @csv' >> workload.csv
sed -i.bak 's/[\"\\]//g' workload.csv

À la place de [0] on met [] pour indiquer « toutes les entrées », on mappe les valeurs sinon le filtre csv râle, et on met un double chevron pour mettre le résultat à la suite du contenu existant du fichier.

Et voilà, on peut désormais importer ce CSV dans un tableur pour le présenter de manière intelligible, avec des jolies colonnes qu’on peut filtrer/trier/annoter/corriger, à des gens qui n’ont pas l’habitude de bosser avec du JSON. Au final on aura eu quand même une petite dizaine de sites à corriger, on a donc pu se permettre de le faire manuellement. Sur un plus gros volume, on serait certainement passé par du scripting Python pour modifier les fichiers de values directement dans tous les dépôts Gitlab concernés, en laissant ensuite faire la magie du déploiement continu 🙂

Migrer sa base de données Gitea de SQLite vers MySQL/MariaDB

29 janvier 2025 à 17:44

Dire que ce sujet m’aura arraché quelques cheveux est un euphémisme, et pourtant dieu sait que j’en ai pas vraiment besoin, ça tombe déjà tout seul. Certes Gitea supporte trois moteurs différents (SQLite, MySQL/MariaDB, PostgreSQL), ça ne l’empêche pas de ne pas proposer de méthode officielle ni d’outil pour migrer une installation existante d’un moteur à l’autre. J’ai donc retroussé mes manches, roté un litre de sang ou deux, mais j’ai trouvé comment faire et surtout réussi. Je vous raconte.

Ceci est principalement une concaténation de ce que j’ai commencé à faire en stream le 17 janvier dernier, et terminé le dimanche 26 suivant. J’étais parti de la base d’un message sur le forum de Gitea cherchant à faire exactement la même chose. J’en avais déduit une série d’étapes que j’ai plus ou moins respectées, assez simple:

  • Dumper la base actuelle via la commande intégrée (qui génère un dump sql de la base SQLite); l’instance doit être démarrée pour ça, because binaire gitea
  • Couper gitea pour éviter un delta
  • Archiver le dossier sur le NAS, pour tout restaurer à l’état initial si besoin
  • Récupérer le fichier sql du dump
  • Lancer une instance vierge de gitea sur MySQL/MariaDB directement avec docker-compose, initialiser l’installation, et dumper la base créée
  • Retirer les INSERT INTO du fichier de dump MySQL, et y ajouter à la fin tous les INSERT INTO du fichier de dump SQLite
  • Importer dans l’instance MySQL cible,  Dans mon cas, MariaDB, mais c’est pareil
  • Reconfigurer son Gitea pour qu’il l’utilise à la place
  • Retirer le fichier sqlite du dossier de données (on l’a toujours dans l’archive, remember)
  • Redémarrer gitea

Et à la vérité, même avec les travaux supplémentaires post-stream, au niveau du fichier de dump lui-même, il s’avère qu’il n’y a pas besoin de faire plus que les trois étapes mentionnées au dessus. Comment est-ce possible !?

Le problème qu’on avait donc au moment de tenter l’import dans l’instance se produisait sur des INSERT donc certains champs contenaient des emojis. La plupart du temps, ça se produit pour des histoires d’encodage au niveau des fichiers ou de la base. C’est d’ailleurs ce qui m’a fait perdre du temps pendant le live, où on a tenté en vain de convertir les fichiers avant de tenter l’import, avec un résultat parfois très surprenant, comme ce passage par UTF-16 puis UTF-8 qui s’est soldé par un fichier aussi compréhensible que du chinois, et pour cause, il était rempli de caractères chinois !

L’image de la satisfaction sur mon visage 🙂

La joie des encodages dans les bases de données

Dans le schéma (qu’on peut consulter via MySQL ou dans le fichier lui-même), beaucoup de champs/tables sont encodées en utf8mb4, c’est à dire UTF-8 avec la possibilité de coder les caractères sur 4 octets. Avec l’embonpoint qu’a pris UTF-8 ces dernières années pour inclure toujours plus d’emojis, c’est une nécessité adoptée par énormément d’applications dont les données sont fournies par les utilisateurs, et les forges git ne sont pas épargnées (WordPress aussi par exemple est passé à l’utf8mb4 il y a un bon moment déjà).

Ce qui m’a interpellé c’est qu’effectivement, l’erreur sur laquelle je butais concernait des emojis, plus précisément ceux que j’ai foutu dans le code de mon premier pipeline Gitea Actions de livraison du LinkFree (autre sujet découvert en live, abonnez-vous/follow toussa toussa). Dans le fichier on voit bien qu’ils sont sur quatre octets. Mais alors, qu’est-ce qui coince ? En effet, en cherchant un peu, on tombe sur un thread qui dit que parfois, le serveur force un autre mode d’encodage si on lui dit pas de faire autrement, et conseille d’utiliser la commande SET NAMES utf8mb4 pour forcer les paramètres de fonctionnement. Plus étrangement, c’est pourtant ce que j’ai au début du fichier de dump, donc pourquoi ne pas en tenir compte ?

Apparemment, c’est ce que fait mon serveur au moment où le client se connecte, je me retrouve en utf8mb3. Et donc, avec la première méthode d’import, celle qu’on conseille dans 99,99% des cas, il n’en tient pas compte. Oui oui, avec mysql gitea <dump.sql, et même si les premières lignes du fichier en question disent de passer en utf8mb4, il n’en tient pas compte.

Un des soucis avec cette méthode d’ailleurs, c’est qu’il abandonne à la première erreur. Ce qui fait qu’on ne sait pas trop combien de lignes sont concernées par le problème. Je change donc mon fusil d’épaule et tente de faire autrement. En effet, le client mysql permet, après la connexion, de « sourcer » un fichier sql depuis le système de fichiers (je vous laisse la page de doc pour en connaitre les détails). En tentant basiquement le coup, j’ai eu raison de vouloir le tenter, mais ça m’a aussi joué un tour : il affiche les erreurs, toutes cette fois, mais aussi un message de retour pour chaque INSERT réussi. Pas de bol, y’en a plus de 30000, et un historique de 9000 lignes seulement dans Windows Terminal. Il va donc falloir ruser.

Je tente une chose: utiliser l’option « -e » de mysql, qui permet de lancer une ou plusieurs requêtes après la connexion, un peu à la manière d’SSH, ce qui rend la main dans le shell juste après (ça connecte, ça exécute, ça déconnecte). Pour pouvoir parcourir les résultats, je renvoie la sortie standard dans un fichier. Et là, très bonne surprise : les erreurs sont renvoyées sur l’erreur standard, mais comme je renvois la sortie standard dans un fichier, je n’ai QUE les erreurs à l’écran !

Il n’y en a pas tant que ça, mais je découvre d’autres erreurs. Je décide de les prendre dans l’ordre et de m’atteler donc au souci d’emojis. Il ne tient pas compte de l’entête du fichier ? Pas grave, on est en mode « -e », j’ajoute donc le SET NAMES en premier avant de faire mon source :

mysql -p$MARIADB_ROOT_PASSWORD -e "set NAMES utf8mb4; source /root/gitea-final.sql;"

Tada, plus d’erreurs d’emojis, mais il reste quelque chose d’encore plus cryptique:

ERROR 1292 (22007) at line 31080 in file: '/root/gitea-final.sql': Truncated incorrect DOUBLE value: 'push failed: remote: Invalid username or password.'
ERROR 1292 (22007) at line 31907 in file: '/root/gitea-final.sql': Truncated incorrect DOUBLE value: '<a href="/Seboss666/collect/commit/79b78d68891f8e7b19c6e7b6968914ea2607d2f7">change last films from 10 to 30 (see #4)'
ERROR 1292 (22007) at line 31910 in file: '/root/gitea-final.sql': Truncated incorrect DOUBLE value: 'Flask framework has an extension flask-bootstrap (as mentioned and used in the mega-tutorial), and Bootstrap was one of the c...'
ERROR 1292 (22007) at line 31911 in file: '/root/gitea-final.sql': Truncated incorrect DOUBLE value: '<a href="/Seboss666/collect/commit/5ad6c4660205e3b5c0c29f92bc9ab8adf042cfc8">change default type for new movies (#3)'
ERROR 1292 (22007) at line 31918 in file: '/root/gitea-final.sql': Truncated incorrect DOUBLE value: '<a href="/Seboss666/k3s_platform/commit/edfe7190ab523b635390f5dff4a6b08f69d59c66">consul migration (issue #7)'

Bon, il n’y en a pas beaucoup, et c’est vite trouvé : y’a un truc avec une syntaxe pourtant correcte de concaténation de caractères à base de « double pipes », avec entre deux, un caractère encodé. En tout cas sur l’instant et avec une recherche rapide sur DDG ça a l’air correct et supporté. Cette fois, le caractère en question est censé vouloir dire « retour à la ligne », l’équivalent du « \n » qu’on rencontre parfois, dans les formats sérialisés notamment (coucou JSON). Mais cette erreur SQL par contre, elle couvre beaucoup de cas d’usages différents à voir les résultats de recherche que je trouve, et qui ne correspondent pas au format de données auquel je fais face.

INSERT INTO `push_mirror` (`id`, `repo_id`, `remote_name`, `remote_address`, `sync_on_commit`, `interval`, `created_unix`, `last_update`, `last_error`) VALUES (1,39,'remote_mirror_f93HyYmLhw','https://github.com/seboss666/ovh-dynhost-helm.git',1,28800000000000,1686342369,1737866664,'push failed: remote: Invalid username or password.' || X'0a' || 'fatal: Authentication failed for ''https://github.com/seboss666/ovh-dynhost-helm.git/''' || X'0a' || ' - remote: Invalid username or password.' || X'0a' || 'fatal: Authentication failed for ''https://github.com/seboss666/ovh-dynhost-helm.git/''' || X'0a' || ' - remote: Invalid username or password.' || X'0a' || 'fatal: Authentication failed for ''https://github.com/seboss666/ovh-dynhost-helm.git/''' || X'0a0a');

Je vous passe la bonne heure à tenter des trucs pourris, à me dire que peut-être je vais bricoler à la main pour retoucher les lignes, voire tenter de réécrire les INSERT en question en utilisant la fonction CONCAT (prévue justement pour concaténer des chaînes de caractères),  avant de tomber sur cet article qui parle d’un comportement avec dBeaver (trousse à outil pour bases de données qui peut être assez puissant), et dont la source m’avait déjà cassé les couilles par le passé chez un client d’LBN: les modes SQL. Dans l’article, il parle d’un mode en particulier, PIPES_AS_CONCAT. Une petite vérification rapide montre que je n’ai effectivement pas ce mode d’activé par défaut, il ne reconnait donc pas la syntaxe de concaténation ! Sur l’instant je peste sur le fait qu’il va vraiment falloir que je me penche sur la configuration de mon instance MariaDB et je tente de modifier ma commande pour en arriver à la version finale:

mysql -p$MARIADB_ROOT_PASSWORD -e 'set NAMES utf8mb4; SET sql_mode=(SELECT CONCAT(@@sql_mode, ",PIPES_AS_CONCAT")); source /root/gitea-final.sql;' > source_result.log

Et là, c’est fini, plus d’erreurs !!!

Au tour de Gitea de se faire reconfigurer la face

En effet, maintenant qu’on a une base fonctionnelle, comment lui dire de l’utiliser ? Là, c’est via la documentation à la fois de gitea lui-même et de l’image docker que la solution sera trouvée.

Déjà pour commencer, j’ai la base de données, mais il me faut l’utilisateur et le mot de passe qui vont bien avec. Un coup de GRANT ALL PRIVILEGES plus tard (oui c’est toujours supporté dans MariaDB), c’est fait. Au début, je me dis que je vais la jouer ceinture/bretelles, à savoir modifier le fichier de conf ET les variables d’environnement pour m’assurer que ça fonctionne comme il faut. Côté fichier, simple, je me rends sur mon NAS, entre dans le dossier de configuration de gitea, édite le fichier avec vi (non, pas vim), et modifie les paramètres de la section [database] pour pointer sur MariaDB (la doc officielle de Gitea est particulièrement complète sur ce point).

Pour les variables d’environnement, c’est un poil plus sport. Toutes mes variables sont actuellement définies directement dans le manifeste deployment, je décide de rester comme ça pour l’instant (j’exclus pas de déporter ça dans un ConfigMap plus tard pour la lisibilité), sauf pour un seul d’entre eux, évident: le mot de passe. Lui, je le fous dans un secret via kubectl et référence celui-ci dans mon déploiement.
En gros, ça donne ça :

Au début j’ai une jolie erreur au démarrage, en vérifiant le fichier de configuration il s’avère que ce que j’avais écrit a été remplacé par le contenu fourni dans les variables d’environnement (ce qui me rassure sur le fonctionnement de l’image finalement), mais j’avais été un peu vite dans les copier/coller et merdé le pointage de la base, mauvais nom d’hôte. Je corrige et réapplique, une minute plus tard, j’entends mon NAS gratter salement. Mais pas d’erreur, je confirme en supprimant le fichier sqlite du dossier de données, tout va bien, c’est donc réel. Je tente un push des modifications sur le déploiement de Gitea pour confirmer, tout est toujours bon !

D’autres erreurs possibles ?

Étant donné que la méthode ressemble quand même un peu à du bricolage, c’est pas impossible. Dans le thread d’origine, le gars a expliqué qu’il a quand même dû reconfigurer son fournisseur d’authentification. Je sais pas si c’est lié directement à la migration ou à un autre point de configuration en lien (l’archi autour a peut-être changé, le mode de déploiement aussi, etc), mais en tout cas, dans mon cas, il s’avère que les rares erreurs sont plus liées à la configuration « par défaut » de mon instance MariaDB qui mériterait un peu plus d’amour (ne serait-ce que pour la mettre à jour).

Et on a la chance de pas avoir de contraintes sur de la clé étrangère ou autre, auquel cas il aurait probablement fallu passer par le snippet de Deblan (alias deblantv sur Twitch) qu’il m’a gentiment écrit pendant le live, qui permet de rajouter les commandes de désactivation des contraintes de clés avant de tenter les INSERT. On n’en a pas eu besoin au final, mais je tenais à le remercier pour sa participation, c’est donc tout naturellement que je l’ai tenu informé sur Mastodon en avant-première des avancées, étant donné que c’est sur ce réseau qu’il a vu que j’étais en live et est passé nous voir.

Enfin voilà, j’ai quand même réussi à faire ce que je voulais, et ça fonctionne comme je l’attendais. C’était pourtant pas gagné 🙂

Gérer son DynHost OVH grâce à Kubernetes et Nixery

15 janvier 2025 à 18:19

Après un retour plutôt positif à ma question de proposer des versions textes de certains sujets abordés pendant certains lives Twitch, on va commencer dans l’ordre avec le plus ancien. Fruit de ma tentative de sauvetage d’un naufrage total du live sur GoDNS, je vous propose donc une solution qui ne nécessite aucun soft dédié (si on met de côté Kubernetes évidemment), et sans avoir besoin de créer une image custom !

C’était mon deuxième live sur YouTube, avant que je ne craque et parte sur Twitch. Je vous remets le lien parce qu’il est moins facile à trouver que les autres, l’ergonomie concernant les lives sur YouTube étant… particulière.

Alors, comme d’hab’, commençons par revenir sur les fondamentaux rapidement, à savoir le DynHost. C’est le nom maison d’OVH pour faire du DNS dynamique. Mais qu’est-ce donc ? Un DNS Dynamique, c’est un dispositif pour mettre fréquemment à jour un enregistrement DNS, notamment un enregistrement A pour une adresse IPv4 qui change fréquemment. Et quand on héberge des trucs chez soi et que votre fournisseur d’accès à Internet ne vous fournit pas d’adresse IPv4 fixe (parce que c’est devenu super rare et super cher), c’est super pratique. Certains connaissent peut-être No-IP, DynDNS, certains fournisseurs étant même directement intégrés dans certains routeurs voire certaines box opérateur.

Bref, c’est donc ce que propose OVH avec son service DynHost. Je vais pas rentrer dans un million de détails, j’ai fait un rôle Ansible à une époque pour gérer le truc avec un service qui s’appelle ddclient, je vous remets l’article que j’avais écrit il y a 4 ans pour comprendre ce qu’il fait, il y a dedans les liens vers les documentations d’OVH sur le sujet.

Le problème pendant le live

En très très gros résumé, je voulais remplacer le ddclient de la VM par un pod Kubernetes (pour l’exercice), mais il y a eu deux gros pépins :

  • je voulais utiliser goDNS, mais la doc m’a indiqué qu’OVH n’était pas supporté (sérieux !?)
  • je n’ai jamais réussi à faire fonctionner ddclient plus d’une fois après le démarrage dans mon pod Kubernetes, sans que je sache vraiment pourquoi

Bref, j’ai fini par avoir une idée à la con, mon cerveau fonctionnant toujours malgré les minutes et le stress du live passant : revenir à l’appel de base de l’URL de la doc d’OVH, et utiliser un autre objet Kubernetes de manière originale.

Cronjob et Nixery ?

Un Cronjob Kubernetes permet d’exécuter une tache finie dans le temps à intervalles plus ou moins régulier, à la manière d’une tâche cron sur un serveur classique. Lancer une requête Web avec Curl semble donc très facile à faire avec un cronjob, ce qui veut dire qu’on a pas besoin d’une image de furieux pour le faire fonctionner. Le curl se résume à ça :

#
curl -s -u $CREDENTIALS "https://www.ovh.com/nic/update?system=dyndns&hostname=$DOMAIN&myip=$(curl -s -4 ifconfig.me/ip)"
#

Donc on a au final deux curl imbriqués l’un dans l’autre, car il faut bien commencer par déterminer sa propre adresse IP, et des services comme ifconfig.co et ifconfig.me sont très bons pour ça. Et comme on gère une IPv4, on force de faire l’appel en v4 pour s’éviter des problèmes. Et donc le résultat de ce premier appel est utilisé directement pour envoyer la mise à jour chez OVH.

Bref, on a notre « job », on sait comment le planifier, reste l’environnement pour l’exécuter. Et je me rends compte que je n’ai jamais parlé de Nixery ici. J’ai découvert le service grâce à l’incontournable Jérôme Petazzoni. Nixery est un service de fourniture d’images « Docker » (on devrait désormais parler d’images OCI parce qu’il n’y a pas que docker dans la vie), qui ont la particularité de ne pas être statiques, au sens où on l’entend habituellement. Comme vous le savez, les images OCI sont construites sur un modèle de « couches » où une couche contient les modifications par rapport à la couche précédente. La magie de Nixery est de construire une image « à la volée » à partir de couches correspondants aux outils dont on a besoin dans l’image. Dans mon cas, si je demande nixery.dev/arm64/shell/curl, il va me construire une image contenant un busybox et curl, pour une architecture ARM 64bit, mes Raspberry Pi donc. Et c’est tout. Le seul inconvénient, c’est qu’il va mettre un poil plus de temps à répondre pour nous fournir le manifeste. Mais c’est super cool du coup de pas avoir à faire ses images soi-même 🙂

On a donc tous les ingrédients. Si on veut faire les choses en quick&dirty, on peut le faire dans un seul fichier que l’on pourrait résumer rapidement comme tel:

---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: dynhost-ovh-cron
  labels:
    app.kubernetes.io/name: dynhost-ovh-cron
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/name: dynhost-ovh-cron
        spec:
          containers:
          - name: dynhost
            image: "nixery.dev/arm64/shell/curl:latest"
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - curl -s -u $CREDENTIALS "https://www.ovh.com/nic/update?system=dyndns&hostname=$DOMAIN&myip=$(curl -s -4 ifconfig.me/ip)"
            env:
            - name: CREDENTIALS
              value: <BasicAuth>
            - name: DOMAIN
              value: <Dynhost.Domain.tld>
          restartPolicy: OnFailure

Comme c’est un peu cracra de foutre les credentials dans le fichier, pendant le live et par la suite, j’ai peaufiné un chart Helm que j’ai enregistré sur mon Gitea, qui fait un miroir sur GitHub donc je vous partage évidemment le lien. Si d’aventure le README n’est pas suffisamment clair, faites moi signe.

Est-ce qu’on peut faire mieux ?

Probablement, mais déjà pour un quick & dirty réalisé sous stress sans plus de préparation, je suis pas peu fier de moi et depuis les pratiquement deux ans que j’ai mis ça en place, ça fonctionne toujours du feu de dieu. Il manque quand même une bonne partie du setup du DynHost qui se fait majoritairement à la main, mais après tout, ce n’est pas un système qu’on est censé industrialiser en permanence.

Et idéalement tout le monde passe à IPv6 et on peut laisser tomber IPv4. Mais ça, vu que même Github le supporte pas encore, on est pas rendus…

Bon, maintenant, quel autre sujet d’ancien live mériterait un article écrit en complément ?

❌