Logo
Overview
Un Talos européen de qualité - Part II - Infra physique

Un Talos européen de qualité - Part II - Infra physique

October 31, 2025
29 min read
part-02

Objectif 🎯

Dans la section précédente, nous avons vu les prérequis via l’installation de notre propre KMS ainsi que la création d’images Talos personnalisées pour nos control planes et workers, avec les extensions nécessaires pour Tailscale et Longhorn.

Pour rappel, l’objectif est de déployer Talos en mode Zero Trust, via le réseau privé Tailnet, i.e. qu’à aucun moment l’endpoint API Talos ou Kubernetes ne sera exposé sur Internet. Seul les control planes seront concernés, dans le sens ou les workers seront déjà accessibles au travers ces derniers. Ils doivent être initialisés avec une configuration Talos qui leur permette de rejoindre le cluster via Tailnet dès le premier boot. Il faudra pour cela utiliser le champ user_data de l’API Hetzner Cloud, qui sert habituellement à injecter un fichier cloud-init. Une fois le nœud connecté au réseau Tailnet, on réapplique la configuration Talos pour finaliser l’installation du cluster et bootstrapper Kubernetes dans la foulée.

Nous allons maintenant voir comment déployer un cluster Talos sur Hetzner Cloud au travers de 2 méthodes différentes au choix :

  • Via Talhelper qui est une solution simple d’approche, mélangeant déclaratif et impératif.
  • Via Terraform avec les providers officiels hcloud et talos. Plus avancée, mais réutilisable, avec une configuration Hcloud et Talos unifiée et seule commande à exécuter, c’est la méthode recommandée dans ce guide pour une approche entièrement GitOps.

Talhelper

Talhelper permet de générer les fichiers de configurations Talos pour chaque nœud, tout en étant GitOps friendly. Le mode opératoire :

  1. Génération des clients secrets.
  2. Définition du schéma de cluster Talos.
  3. Génération des fichiers de configuration Talos depuis ce schéma.
  4. Création l’infra physique via l’utilitaire hcloud (mode impératif), en injectant les fichiers de configuration Talos générés précédemment dans le champ user_data.
  5. Générer les commandes pour réappliquer les configs et bootstrapper kube.

Création de la config Talos en mode déclaratif

Selon l’architecture cible décrite ici, le fichier talconfig.yaml ressemblerait à ça :

talconfig.yaml
clusterName: ohmytalos-dev
talosVersion: v1.12.2
kubernetesVersion: v1.35.0
endpoint: https://ohmytalos-dev-control-plane-nbg1:6443
controlPlane:
talosImageURL: factory.talos.dev/hcloud-installer/4a0d65c669d46663f377e7161e50cfd570c401f26fd9e7bda34a0216b6f1922b
extensionServices:
- name: tailscale
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
volumes:
- name: STATE
encryption:
provider: luks2
keys:
- nodeID: {}
slot: 0
- name: EPHEMERAL
encryption:
provider: luks2
keys:
- static:
passphrase: ${VOLUME_ENCRYPTION_PASSPHRASE}
slot: 0
lockToState: true
worker:
talosImageURL: factory.talos.dev/hcloud-installer/613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245
volumes:
- name: STATE
encryption:
provider: luks2
keys:
- nodeID: {}
slot: 0
- name: EPHEMERAL
encryption:
provider: luks2
keys:
- static:
passphrase: ${VOLUME_ENCRYPTION_PASSPHRASE}
slot: 0
lockToState: true
cniConfig:
name: none
clusterPodNets:
- 10.42.0.0/16
clusterSvcNets:
- 10.43.0.0/16
nodes:
- hostname: control-plane-nbg1
ipAddress: ohmytalos-dev-control-plane-nbg1
installDisk: /dev/sda
controlPlane: true
ignoreHostname: true
- hostname: control-plane-fsn1
ipAddress: ohmytalos-dev-control-plane-fsn1
installDisk: /dev/sda
controlPlane: true
ignoreHostname: true
- hostname: control-plane-hel1
ipAddress: ohmytalos-dev-control-plane-hel1
installDisk: /dev/sda
controlPlane: true
ignoreHostname: true
- hostname: worker
ipAddress: 10.0.1.1, 10.0.1.2, 10.0.1.3
installDisk: /dev/sda
ignoreHostname: true
nodeLabels:
node.longhorn.io/create-default-disk: config
nodeAnnotations:
node.longhorn.io/default-disks-config: '[{"allowScheduling":true,"name":"system","path":"/var/lib/longhorn","tags":["local"]}]'
node.longhorn.io/default-node-tags: '["worker"]'
patches:
- |-
- op: add
path: /machine/kubelet/extraConfig
value:
imageGCHighThresholdPercent: 55
imageGCLowThresholdPercent: 50
- hostname: storage
ipAddress: 10.0.2.1, 10.0.2.2
installDisk: /dev/sda
ignoreHostname: true
nodeLabels:
node.kubernetes.io/exclude-from-external-load-balancers: "true"
node.kubernetes.io/role: storage
node.longhorn.io/create-default-disk: config
nodeAnnotations:
node.longhorn.io/default-disks-config: '[{"allowScheduling":true,"name":"system","path":"/var/lib/longhorn","tags":["local"]},{"allowScheduling":true,"name":"volume","path":"/var/mnt/longhorn","tags":["volume"]}]'
node.longhorn.io/default-node-tags: '["storage"]'
userVolumes:
- name: longhorn
encryption:
provider: luks2
keys:
- static:
passphrase: ${VOLUME_ENCRYPTION_PASSPHRASE}
slot: 0
lockToState: true
provisioning:
diskSelector:
match: disk.dev_path == '/dev/sdb'
grow: true
minSize: 40Gi
patches:
- |-
- op: add
path: /machine/kubelet/extraConfig
value:
imageGCHighThresholdPercent: 55
imageGCLowThresholdPercent: 50
registerWithTaints:
- key: node-role.kubernetes.io/storage
effect: NoSchedule
patches:
- |-
- op: add
path: /cluster/proxy
value:
disabled: true
- op: add
path: /cluster/externalCloudProvider
value:
enabled: true

Rajouter le fichier suivant pour les variables d’environnement secrètes :

talenv.sops.yaml
TS_AUTHKEY: tskey-auth-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
VOLUME_ENCRYPTION_PASSPHRASE: supersecretpassphrase

Lancer les commandes suivantes pour générer les fichiers de conf pour chaque pool de nœud.

Terminal window
talhelper gensecret > talsecret.sops.yaml
sops -e -i talsecret.sops.yaml
sops -e -i talenv.sops.yaml
talhelper genconfig

Création infrastructure en mode impératif

On passe ensuite à la partie fastidieuse pour créer notre cluster physique en mode impératif via le CLI Hetzner Cloud. Assurez-vous d’être sur le bon contexte de projet via hcloud context.

Terminal window
# network
hcloud network create --name ohmytalos --ip-range 10.0.0.0/10
hcloud network add-subnet --type cloud --network-zone eu-central --ip-range 10.0.0.0/24 ohmytalos
hcloud network add-subnet --type cloud --network-zone eu-central --ip-range 10.0.1.0/24 ohmytalos
hcloud network add-subnet --type cloud --network-zone eu-central --ip-range 10.0.2.0/24 ohmytalos
hcloud firewall create --name ohmytalos
CP_SNAPSHOT_ID=$(hcloud image list -t snapshot -l name=cp,arch=amd64 -o columns=id | tail -n 1)
WK_SNAPSHOT_ID=$(hcloud image list -t snapshot -l name=wk,arch=amd64 -o columns=id | tail -n 1)
# control planes
hcloud server create --name ohmytalos-dev-control-plane-nbg1 --image $CP_SNAPSHOT_ID --type cx23 --location nbg1 --firewall ohmytalos --user-data-from-file clusterconfig/ohmytalos-dev-control-plane-nbg1.yaml
hcloud server attach-to-network --network ohmytalos --ip 10.0.0.2 ohmytalos-dev-control-plane-nbg1
# commandes similaires à chaque node...
# pensez à créer les 2 volumes et à les rattacher aux serveurs de nœuds de storage
Note

Noter --user-data-from-file qui permet d’injecter le fichier de configuration Talos. Il sera appliqué au premier boot de la machine, l’inscrivant dans le réseau Tailnet.

Une fois les machines démarrées, vous devriez voir les 3 control planes apparaître dans votre Tailnet. Approuvez-les si nécessaire, puis lancez les commandes générées via talhelper gencommand apply, talhelper gencommand bootstrap, talhelper gencommand kubeconfig pour finaliser l’installation du cluster et bootstrapper Kubernetes. Cela devrait donner quelque chose comme :

Terminal window
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.0.2 --file=./clusterconfig/ohmytalos-dev-control-plane-nbg1.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.0.3 --file=./clusterconfig/ohmytalos-dev-control-plane-fsn1.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.0.4 --file=./clusterconfig/ohmytalos-dev-control-plane-hel1.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.1.1 --file=./clusterconfig/ohmytalos-dev-worker.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.1.2 --file=./clusterconfig/ohmytalos-dev-worker.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.1.3 --file=./clusterconfig/ohmytalos-dev-worker.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.2.1 --file=./clusterconfig/ohmytalos-dev-storage.yaml;
talosctl apply-config --talosconfig=./clusterconfig/talosconfig --nodes=10.0.2.2 --file=./clusterconfig/ohmytalos-dev-storage.yaml;
talosctl bootstrap --talosconfig=./clusterconfig/talosconfig --nodes=10.0.0.2;
talosctl kubeconfig --talosconfig=./clusterconfig/talosconfig --nodes=10.0.0.2;

Et voilà le cluster est prêt ! Vous pouvez maintenant tester un kubectl get nodes pour vous assurer de la présence de tous vos nodes. Ils seront en status NotReady pour l’instant, car nous n’avons pas encore installé de CNI (setté à none dans la config).

Conclusion sur Talhelper

Cette solution est intéressante dans le sens où l’on n’a pas à mettre les mains dans du code Terraform pur et dur, l’approche est donc plus directe. En échange, c’est forcément un peu plus impératif et manuel, même s’il n’est pas compliqué de se concocter un petit script bash pour automatiser au moins la création de l’infra physique Hcloud. Idéal en tout cas si l’on a déjà une infra Talos physique existante accessible.

L’autre inconvénient ici est que l’on est un peu obligé de décrire l’infrastructure deux fois, une via le schéma talconfig, et l’autre via les commandes Hcloud impératives (type de serveur, location, network, image, etc.).

Terraform

Nous le ferons avec l’aide du provider hcloud pour la création de l’infra en interrogeant l’API de Hetzner Cloud. Il sera utilisé en conjonction avec le provider talos qui gérera la configuration du cluster et le bootstrapping de Kubernetes. Le but ici est d’avoir un kube fonctionnel en une passe joignable dès la fin de terraform apply.

Pour la mise en place de l’infrastructure post-kube (CNI, Longhorn, ingress, monitoring), de nombreux guides basculent rapidement vers FluxCD, mais je ne suis pas très fan de cette approche. Je préfère garder la gestion de l’entièreté de l’infra avec Terraform/OpenTofu, et réserver FluxCD pour le déploiement des applications métiers uniquement. Le but étant de bien séparer les responsabilités entre équipe Infra / Dev.

De plus je divise aussi la partie infra en 2 modules Terraform distincts, avec séparation de la couche “physique” (Hcloud et config Talos) de la couche “logique” dédiée aux manifests Kubernetes. Cela simplifie grandement la maintenance du code Terraform ainsi que le process de mise à jour de chaque composant, car cela fonctionne de manière très différente dans chaque module. Voyer ça comme monter une baraque :

  1. Fondations et charpentes : Hcloud et cluster Talos via Terraform
  2. Murs et toiture : CNI, stockage, base de données, ingress, monitoring via Terraform
  3. Aménagement intérieur : déploiement applicatif métier via FluxCD

Cette section s’attaque à la partie 1.

Remark (terraform-hcloud-kubernetes)

Il est à noter que le module hcloud officiel de déploiement de Talos, mélange l’étape 1 et 2 en un seul module. Il va même plus loin en injectant carrément les manifests généré par Helm dans la config Talos, ce qui a la fâcheuse conséquence de générer un state terraform énorme.

Mais il a le mérite d’être blindé et testé par la communauté, à défaut d’être simpliste techniquement. À l’écriture de ce guide, il a aussi 2 autres inconvénients : pas de gestion de volumes hcloud ni de schematic image par node pool. Mais il possède un autoscaler préconfiguré, pour ceux ayant ce genre de besoin.

Le sujet de l’autoscaling ne sera pas abordé dans ce guide, mais il pourra bien sûr être assez aisément intégré dans votre module terraform en installant son chart dédié 😊.

Initialisation

Reprenons le projet de l’étape précédente et préparons la structure suivante :

ohmytalos-cluster/
├── clusters
│ └── dev-hcloud
│ ├── .envrc
│ ├── cluster.tf
│ ├── outputs.tf
│ ├── terraform.tf
│ └── variables.tf
├── modules
│ └── hcloud
│ ├── firewalls.tf
│ ├── locals.tf
│ ├── network.tf
│ ├── outputs.tf
│ ├── servers.tf
│ ├── talos.tf
│ ├── terraform.tf
│ ├── variables.tf
│ └── volumes.tf
└── packer
├── .envrc
├── hcloud.pkr.hcl
├── mise.toml
├── schematic-cp.yaml
└── schematic-wk.yaml

Le module modules/hcloud contiendra toute la logique pour créer les ressources Hetzner Cloud et installer Talos sous forme de nodes pools. Il pourra être réutilisé ainsi autant que nécessaire sur différents clusters (prod, staging, etc.). Le répertoire clusters/dev-hcloud contiendra uniquement la configuration spécifique à notre cluster en cours.

State Terraform

Pour l’hébergement de notre state Terraform, nous allons prendre un bucket S3 ohmytalos-dev dédié au cluster (ici OVH mais cela doit être plus ou moins identique chez n’importe quel autre provider).

Vu que ce n’est pas beaucoup plus compliqué, nous allons chiffrer le state via SSE-C. Au niveau du projet terraform en lui-même, seuls deux variables seront nécessaires, le token API Hetzner Cloud et le token Tailscale.

Danger

Le state contiendra les infos les plus sensibles de notre infra, comme le talosconfig, les machines config, etc. Toutefois l’impact restera mitigé en cas de fuite ici puisque le endpoint sera inaccessible en dehors du réseau Tailnet, ce qui nous laissera le temps de réagir en renouvelant toutes les clés de notre PKI.

Le token API Hetzner ne sera pas dans le state, étant uniquement utilisé localement pour l’accès API via le provider. Il y aura toutefois le token Tailnet, donc assurez-vous que l’enregistrement d’un device soit exclusivement approuvable par un admin Tailscale, ou encore mieux utiliser Tailnet Lock pour les plus paranos.

Allez sur votre Vaultwarden et créez les clés suivantes dans une collection dédiée Terraform - Hcloud Talos (noter son GUID collectionId pour la suite) :

Nom de la cléDescription
terraform_state_s3Les identifiants d’accès au bucket S3, à générer côté OVH via un utilisateur S3 dédié, indiquer sa clé d’accès en username et la clé secrète en password
terraform_state_sse_cLa clé de chiffrement SSE-C, utiliser la commande openssl rand -base64 32 pour générer la clé
hcloud_tokenLe token API Hetzner Cloud, à générer dans votre projet Hcloud
ts_auth_keyle token d’authentification Tailscale, à générer dans votre compte Tailscale
volume_encryption_passphrasela passphrase de chiffrement des volumes

Exporter vos variables comme suit :

clusters/dev-hcloud/.envrc
BW_SESSION="$(bw unlock --raw)"
COLLECTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ITEMS=$(bw list items --collectionid $COLLECTION_ID --session $BW_SESSION)
bw_field() {
echo "$ITEMS" | jq -r --arg field "$1" --arg name "$2" \
'.[] | select(.name==$name) | .login[$field]'
}
export AWS_ACCESS_KEY=$(bw_field username terraform_state_s3)
export AWS_SECRET_KEY=$(bw_field password terraform_state_s3)
export AWS_SSE_CUSTOMER_KEY=$(bw_field password terraform_state_sse_c)
export TF_VAR_hcloud_token=$(bw_field password hcloud_token)
export TF_VAR_ts_auth_key=$(bw_field password ts_auth_key)
export TF_VAR_volume_encryption_passphrase=$(bw_field password volume_encryption_passphrase)
Tip (perf)

Petite astuce ici, nous récupérons une seule fois la liste des items de la collection dans une variable ITEMS pour éviter d’appeler bw à chaque variable, ce qui serait bien trop lent.

Toutes les variables d’env TF_VAR_* seront automatiquement reconnues par Terraform comme des variables d’entrée.

Assurer vous toujours de lancer bw sync après maj d’une variable secrète, puis placez-vous dans le répertoire clusters/dev-hcloud et lancer un direnv allow. Le mot de passe maître du vault vous sera demandé. Assurez-vous que vos variables d’env sont bien chargées.

Dans le répertoire modules/hcloud, on ajoute les dépendances aux providers hcloud et talos et on prépare la variable token Hetzner Cloud.

modules/hcloud/terraform.tf
terraform {
required_version = ">= 1.13.0"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = ">= 1.52.0"
}
talos = {
source = "siderolabs/talos"
version = ">= 0.10.0"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}

Côté config cluster, créer les fichiers terraform.tf pour initialiser le backend S3 et variables.tf pour nos 3 variables d’entrée. On prépare le module dans cluster.tf.

clusters/dev-hcloud/terraform.tf
terraform {
required_version = ">= 1.13.0"
backend "s3" {
endpoints = {
s3 = "https://s3.gra.io.cloud.ovh.net"
}
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
region = "gra"
bucket = "ohmytalos-dev"
key = "terraform/hcloud.tfstate"
encrypt = true
}
}

Lancer un premier terraform init pour initialiser le backend S3 puis un terraform plan pour vérifier que tout est ok.

Variables

Le plus simple pour démarrer est d’exprimer la meilleur API de ce que l’on a besoin concrètement pour la création de notre cluster. On va donc définir toutes les variables d’entrée du module hcloud dans variables.tf.

modules/hcloud/variables.tf
variable "hcloud_token" {
type = string
sensitive = true
}
variable "talos_version" {
type = string
}
variable "talos_endpoints" {
type = list(string)
default = []
}
variable "talos_snapshots" {
type = list(object({
name = string
schematic_id = string
}))
}
variable "cluster_endpoint" {
type = string
default = null
}
variable "kubernetes_version" {
type = string
}
variable "cluster_name" {
type = string
}
variable "network_zone" {
type = string
}
variable "network_ipv4_cidr" {
type = string
}
variable "existing_network_id" {
type = string
default = null
}
variable "firewall_kube_api_source" {
type = list(string)
default = [
"127.0.0.1",
"::1"
]
}
variable "firewall_talos_api_source" {
type = list(string)
default = [
"127.0.0.1",
"::1"
]
}
variable "config_patches" {
type = list(string)
default = []
}
variable "control_planes_image_name" {
type = string
}
variable "control_planes_placement_group" {
type = string
}
variable "control_planes_config_patches" {
type = list(string)
default = []
}
variable "control_planes_ipv4_cidr" {
type = string
}
variable "control_planes" {
type = list(object({
name = string
server_type = string
location = string
ip = string
}))
}
variable "worker_nodepools" {
type = list(object({
name = string
server_type = string
location = string
ipv4_cidr = string
placement_group = optional(string)
image_name = optional(string)
config_patches = optional(list(string))
config_patches_apply = optional(list(string))
nodes = list(object({
name = string
ip = string
server_type = optional(string)
location = optional(string)
}))
volumes = optional(list(object({
name = string
size = number
})))
}))
}
NomDescription
talos_versionLa version de Talos à utiliser sur les images d’installation. Ne déclenche aucune mise à jour, permet surtout d’afficher la liste des commandes talosctl à exécuter pour mettre à jour avec la bonne version d’image pour chaque nœud.
talos_endpointsListe des endpoints pour l’accès à l’API des nœuds Talos. Pour l’apply de la configuration, seul le 1er endpoint sera utilisé. Si vide, le hostname du 1er control plane sera utilisé, permettant au MagicDNS de Tailscale de s’exprimer.
talos_snapshotsDéfinition des snapshots disponibles créés précédemment via packer.
kubernetes_versionVersion vanilla de kubernetes à installer au bootstrap. Attention, au contraire de la version talos dont la maj est manuelle, celle-ci déclenche un mise à jour directement, il est recommandé d’appliquer node par node via l’utilisation de l’argument target de terraform, ou d’utiliser talosctl upgrade-k8s.
cluster_endpointLe endpoint d’accès à Kube API server. Si vide utilise le hostname généré du 1er control plane. Est exporté dans la génération du kubeconfig.
cluster_nameLe nom du cluster kube. Servira de préfixe au niveau des hostnames de l’ensemble des nœuds.
existing_network_idID Hcloud du réseau existant à utiliser. Si vide création d’un nouveau réseau de même nom que le cluster.
network_zoneZone géographique du réseau Hcloud en cas de création.
network_ipv4_cidrPlage CIDR IPv4 du réseau Hcloud en cas de création. C’est ici que l’on utilisera 10.0.0.0/10, laissant la place à d’autres réseaux possible dans le même projet.
firewall_talos_api_sourceListe des IPs à autoriser sur l’API Talos port 50000. Uniquement valable pour l’accès externe. Du fait du mode Zero Trust, on laissera la valeur par défaut (aucun accès).
firewall_kube_api_sourceListe des IPs à autoriser sur l’API Kube port 6443. Uniquement valable pour l’accès externe. Du fait du mode Zero Trust, on laissera la valeur par défaut (aucun accès).
config_patchesListe des patchs de config Talos à appliquer sur l’ensemble des nœuds. Notamment utile pour la configuration du réseau du cluster (pods et service CIDR, CNI, etc.).
control_planes_image_nameNom de l’image snapshot à utiliser pour l’installation des panneaux de contrôles.
control_planes_placement_groupGroupe de placement des serveurs de panneau de contrôle pour optimiser la disponibilité. Inutile si locations différentes.
control_planes_config_patchesListe des patchs de config Talos à appliquer sur les panneaux de contrôles. Utile pour la configuration de l’extension Tailscale.
control_planes_ipv4_cidrCIDR du Subnet dédié aux nœuds de panneau de contrôle.
control_planesDéfinition des panneaux de contrôle, au nombre obligatoirement impair pour le quorum. Possibilité de définir hostname, différent emplacement géographique, type de serveur, adresse IP privée selon la plage CIDR pré-définie.
worker_nodepoolsDéfinition des pools de nœuds de travail.
worker_nodepools.*.nameNom du pool. Utilisé dans les hostnames des nœuds du même pool.
worker_nodepools.*.server_typeType de serveur Hcloud par défaut à utiliser pour les nœuds du pool. Peut être surchargé sur chaque node.
worker_nodepools.*.locationEmplacement géographique par défaut des nœuds du pool. Peut être surchargé sur chaque node.
worker_nodepools.*.image_nameNom de l’image snapshot à utiliser pour l’installation des nœuds de ce pool.
worker_nodepools.*.placement_groupGroupe de placement des nœuds de travail pour optimiser la disponibilité. Limite le pool à 10 nœuds max.
worker_nodepools.*.config_patchesListe des patchs de config Talos à appliquer sur les nœuds du pool. Notamment pour la configuration des labels, volumes, teintes, etc.
worker_nodepools.*.config_patches_applyListe des patchs de config Talos à appliquer sur les nœuds du pool uniquement après que la machine soit créée et bootée au moment du apply. Utile pour les configs dépendant de conditions post-installation, typiquement le montage d’un volume externe.
worker_nodepools.*.ipv4_cidrCIDR du subnet dédié au pool.
worker_nodepools.*.nodesListe des nœuds de travail dans le pool, identifiés par leur nom de suffixe et leur adresse IP privées. Possibilité d’y surcharger le type de serveur et la localisation.
worker_nodepools.*.volumesListe des volumes Hcloud à créer et rattacher à chaque nœud du pool. Déclarer ici 2 volumes sur un pool de 3 nœud équivaut à créer 6 volumes au total, chaque nœud ayant 2 volumes montés.

Déclaration

Ceci fait, voilà comment on peut utiliser ces variables pour déclarer notre cluster complet selon notre architecture cible, découpé en un fichier Terraform unique et plusieurs fichiers de config yaml.

clusters/dev-hcloud/cluster.tf
module "hcloud_talos" {
source = "../../modules/hcloud"
hcloud_token = var.hcloud_token
talos_version = "v1.12.2"
talos_snapshots = [
{
name = "cp",
schematic_id = "4a0d65c669d46663f377e7161e50cfd570c401f26fd9e7bda34a0216b6f1922b"
},
{
name = "wk",
schematic_id = "613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245"
},
]
kubernetes_version = "v1.35.0"
cluster_name = "ohmytalos-dev"
network_zone = "eu-central"
network_ipv4_cidr = "10.0.0.0/10"
config_patches = [
file("${path.module}/patches/volume-state.yaml"),
templatefile("${path.module}/patches/volume-ephemeral.yaml", {
volume_encryption_passphrase = var.volume_encryption_passphrase
}),
file("${path.module}/patches/config-cluster.yaml"),
]
control_planes_ipv4_cidr = "10.0.0.0/24"
control_planes_image_name = "cp"
control_planes_config_patches = [
templatefile("${path.module}/patches/extension-tailscale.yaml", {
ts_auth_key = var.ts_auth_key
}),
yamlencode({
cluster = {
extraManifests = [
"https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/main/deploy/standalone-install.yaml"
]
}
})
]
control_planes = [
{
name = "nbg1"
server_type = "cpx22"
location = "nbg1"
ip = "10.0.0.2"
},
{
name = "fsn1"
server_type = "cpx22"
location = "fsn1"
ip = "10.0.0.3"
},
{
name = "hel1"
server_type = "cpx22"
location = "hel1"
ip = "10.0.0.4"
}
]
worker_nodepools = [
{
name = "worker"
server_type = "cpx32"
location = "nbg1"
image_name = "wk"
placement_group = "workers"
ipv4_cidr = "10.0.1.0/24"
nodes = [
{ name = "ndk", ip = "10.0.1.1" },
{ name = "opb", ip = "10.0.1.2" },
{ name = "ozi", ip = "10.0.1.3" }
]
config_patches = [
file("${path.module}/patches/config-machine-worker.yaml")
]
},
{
name = "storage"
server_type = "cpx32"
location = "nbg1"
image_name = "wk"
placement_group = "storages"
ipv4_cidr = "10.0.2.0/24"
nodes = [
{ name = "vsh", ip = "10.0.2.1" },
{ name = "wty", ip = "10.0.2.2" }
]
volumes = [
{ name = "longhorn", size = 80 }
]
config_patches = [
file("${path.module}/patches/config-machine-storage.yaml")
]
config_patches_apply = [
templatefile("${path.module}/patches/volume-longhorn.yaml", {
volume_encryption_passphrase = var.volume_encryption_passphrase
})
]
}
]
}
Explanation

Plutôt limpide ! Avantage par rapport à Talhelper, la configuration Talos se greffe en fonction de l’infra physique, et non l’inverse. On peut ainsi plus facilement raisonner en terme d’infra (type de serveur, emplacement, réseau, etc.) et de config (cluster, stockage, extensions, etc.) de manière séparée, et tout se fait en une seule fois. Reste plus qu’à écrire la logique…

Points notables :

  1. talos_snapshots : On précise les 2 snapshots disponibles créées précédemment via packer.
  2. config_patches : On applique les patchs communs à l’ensemble des nœuds, notamment la configuration du réseau du cluster (CIDR pods et services, CNI, etc.) et chiffrement des volumes systèmes.
  3. control_planes_config_patches : On y configure l’extension Tailscale ainsi que le contrôleur d’approbation automatique des certificats kubelet du fait de l’activation rotate-server-certificates.
  4. control_planes : On déclare les 3 panneaux de contrôle, chacun dans un emplacement géographique différent pour la résilience.
  5. worker_nodepools : Et enfin tous nos nodepools, chacun dans leur propre subnet. Je choisis de nommer les nœuds des différents pools selon un suffixe aléatoire fixe. Libre à vous de faire autrement. On utilise placement_group pour optimiser la disponibilité des nœuds en cas de défaillance (limité jusqu’à 10). Préciser le volume externe à utiliser pour chaque nœud du pool de storage. Utiliser config_patches_apply pour appliquer la configuration de volume Longhorn uniquement après que le volume soit montée.

Implémentation

modules/hcloud/locals.tf
locals {
talos_snapshots = { for s in var.talos_snapshots : s.name => s.schematic_id }
talos_endpoints = length(var.talos_endpoints) > 0 ? var.talos_endpoints : [for s in local.control_planes : "${var.cluster_name}-${s.name}"]
machine_base_config = {
kubelet = {
nodeIP = {
validSubnets = concat(
[
var.control_planes_ipv4_cidr,
],
[
for s in hcloud_network_subnet.worker : s.ip_range
]
)
}
extraArgs = {
"rotate-server-certificates" = true
}
}
features = {
hostDNS = {
enabled = true
forwardKubeDNSToHost = true
resolveMemberNames = true
}
}
time = {
servers = [
"ntp1.hetzner.de",
"ntp2.hetzner.com",
"ntp3.hetzner.net",
"time.cloudflare.com"
]
}
}
machine_control_plane_config = merge(local.machine_base_config, {
features = merge(local.machine_base_config.features, {
kubernetesTalosAPIAccess = {
enabled = true
allowedRoles = [
"os:reader",
"os:etcd:backup"
]
allowedKubernetesNamespaces = [
"kube-system",
]
}
})
})
cluster_control_plane_config = {
controllerManager = {
extraArgs = {
"bind-address" = "0.0.0.0"
}
}
etcd = {
advertisedSubnets = [
var.control_planes_ipv4_cidr
]
extraArgs = {
"listen-metrics-urls" = "http://0.0.0.0:2381"
}
}
scheduler = {
extraArgs = {
"bind-address" = "0.0.0.0"
}
}
}
control_planes = [
for i, s in var.control_planes : {
name = "control-plane-${s.name}"
server_type = s.server_type
location = s.location
machine_type = "controlplane"
firewall_ids = [hcloud_firewall.talos_api.id, hcloud_firewall.kube_api.id]
private_ipv4 = s.ip
placement_group_id = var.control_planes_placement_group != null ? hcloud_placement_group.this[var.control_planes_placement_group].id : null
image_name = coalesce(var.control_planes_image_name, "default")
machine_config = local.machine_control_plane_config
cluster_config = local.cluster_control_plane_config
config_patches = var.control_planes_config_patches
config_patches_apply = []
}
]
workers = flatten([
for i, np in var.worker_nodepools : [
for index, n in np.nodes : {
name = "${np.name}-${n.name}"
server_type = coalesce(n.server_type, np.server_type)
location = coalesce(n.location, np.location)
machine_type = "worker"
firewall_ids = [hcloud_firewall.talos_api.id]
private_ipv4 = n.ip
placement_group_id = np.placement_group != null ? hcloud_placement_group.this[np.placement_group].id : null
image_name = coalesce(np.image_name, "default")
machine_config = local.machine_base_config
cluster_config = {}
config_patches = coalesce(np.config_patches, [])
config_patches_apply = coalesce(np.config_patches_apply, [])
volumes = coalesce(np.volumes, [])
}
]
])
servers = [for s in concat(local.control_planes, local.workers) : merge(s, {
config_patches = concat(
[
yamlencode({
machine = merge(
s.machine_config,
{
install = {
image = "factory.talos.dev/hcloud-installer/${local.talos_snapshots[s.image_name]}:${var.talos_version}"
}
}
)
cluster = s.cluster_config
})
],
var.config_patches,
coalesce(s.config_patches, [])
)
})]
volumes = flatten([
for i, s in local.workers : [
for v in s.volumes : {
server_name = s.name
location = s.location
name = "${s.name}-${v.name}"
size = v.size
}
]
])
}
Explanation

Ce fichier ne sert essentiellement qu’à construire des structures de données complexes à partir des variables d’entrée, pour simplifier la logique dans les autres fichiers. On y retrouve la configuration Talos de base, celle spécifique aux panneaux de contrôle, la configuration du cluster, et la construction des listes de serveurs et volumes à créer.

On n’oublie pas de reporter les outputs dans le module terraform principal pour les exporter dans le state.

clusters/dev-hcloud/outputs.tf
output "talosconfig" {
value = module.hcloud_talos.talosconfig
sensitive = true
}
output "kubeconfig_command" {
value = module.hcloud_talos.kubeconfig_command
}
output "health_command" {
value = module.hcloud_talos.health_command
}
output "upgrade_command" {
value = module.hcloud_talos.upgrade_command
}

Et voilà, c’est l’heure du grand test, lancer terraform apply et prier ! Encore une fois, si l’approbation des nœuds des control planes au réseau Tailnet est manuel, penser à accepter sur l’admin Tailscale les nouveaux devices qui devraient apparaître après la création des serveurs, i.e. au moment où terraform lance les opérations module.hcloud_talos.talos_machine_configuration_apply.this.

Tip

Si Tailnet Lock est actif, utiliser les commandes tailscale lock status pour choper le nodekey puis tailscale lock sign nodekey:xxx.

Le terraform apply terminé, il ne reste plus qu’à récupérer le talosconfig et le kubeconfig :

Terminal window
terraform output -raw talosconfig > ~/.talos/config
talosctl -n 10.0.0.2 kubeconfig

Utiliser talosctl -n 10.0.0.2 dashboard pour accéder au TUI Talos qui devrait afficher l’ensemble des composants cœur de kube healthy (kubelet, api-server, controller-manager, scheduler), mais en état not ready à gauche.

Lancer kubectl get nodes -o wide ou plutôt kgno -o wide pour vérifier l’état des nœuds. Après quelques minutes, ils devraient tous apparaître en état NotReady, attendant bien sagement l’installation du CNI.

NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ohmytalos-dev-control-plane-nbg1 NotReady control-plane 6m46s v1.35.0 10.0.0.2 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-control-plane-fsn1 NotReady control-plane 4m9s v1.35.0 10.0.0.3 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-control-plane-hel1 NotReady control-plane 6m39s v1.35.0 10.0.0.4 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-worker-ndk NotReady <none> 6m1s v1.35.0 10.0.1.1 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-worker-opb NotReady <none> 5m43s v1.35.0 10.0.1.2 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-worker-ozi NotReady <none> 5m43s v1.35.0 10.0.1.3 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-storage-vsh NotReady <none> 5m22s v1.35.0 10.0.2.1 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6
ohmytalos-dev-storage-wty NotReady <none> 6m6s v1.35.0 10.0.2.2 <none> Talos (v1.12.2) 6.18.5-talos containerd://2.1.6

Ne lancez pas la commande talosctl -n 10.0.0.2 health pour le moment, puisque le cluster n’est pas encore opérationnel, bien que physiquement accessible.

Conclusion

Nous en avons terminé sur la création du cluster physique, suite à la prochaine section pour l’installation de l’infra logicielle kube.