Proxmox, OpenTofu et Talos, Kubernetes as code

Dans un contexte où la gestion des infrastructures et des systèmes d’informations devient de plus en plus complexe, l’Infrastructure as Code (IaC) s’impose comme une solution incontournable pour automatiser, standardiser et optimiser les déploiements.

Parmi les outils phares de cet écosystème, Proxmox, OpenTofu et Talos se distinguent par leur efficacité et leur complémentarité. Je vous propose d’utiliser ces outils pour déployer des machines virtuelles dans Proxmox grâce à OpenTofu, et y instancier un cluster Kubernetes avec Talos .

Proxmox est une plateforme de virtualisation open-source qui permet de gérer des machines virtuelles et des conteneurs. OpenTofu, un fork de Terraform, est utilisé pour l’automatisation de l’infrastructure, permettant de définir et de provisionner des ressources de manière déclarative. Talos, quant à lui, est un système d’exploitation minimaliste conçu spécifiquement pour exécuter Kubernetes, offrant une empreinte réduite et une sécurité renforcée.

Toujours dans l’optique d’héberger mes services localement, j’ai décidé d’utiliser ces outils pour mettre en place une infrastructure complexe, robuste et capable d’être mise à l’échelle.

Pour éviter une redite, je me suis basé sur plusieurs articles concernant Proxmox et OpenTofu :

Utiliser Terraform ou OpenTofu pour créer une VM dans ProxmoxSuite à un article portant sur l’utilisation de Terraform pour créer des LXC pour Proxmox, je souhaite approfondir le sujet en abordant cette fois la création de machines virtuelles. Prérequis : Une connaissance préalable de Terraform, des droits d’administration sur Proxmox, et la capacité à effectuer des manipulations sans contraintes majeures.J.HOMMET.NETJulien HOMMETDéployer plusieurs VM avec Terraform ou OpenTofu dans ProxmoxLa puissance des outils comme Terraform et OpenTofu résident dans leur capacité de mise à l’échelle. Créer une machine virtuelle est assez rapide, mais créons plusieurs machines pour exploiter au mieux l’outil. J’utilise OpenTofu depuis début 2025 sur différentes infrastructures Proxmox, en ligne et déconnectées. Les exemples de code ci-dessousJ.HOMMET.NETJulien HOMMETVolontairement, je pars du principe que vous avez déjà une connaissance du monde IaC, d’OpenTofu et de Kubernetes.

À titre d’exemple, je créerai deux machines virtuelles : une pour le « control plane » et l’autre pour le « worker ». Talos et Kubernetes seront préconfigurés pour utiliser Cilium comme CNI. Il n’y aura pas de CSI, pas d’exemple de déploiement d’application ou d’observabilité. L’objectif est uniquement de mettre en place le cluster et de le rendre fonctionnel.

Contexte Proxmox (8.4.1)#

Je n’utilise qu’une seule machine physique pour la virtualisation, au lieu d’en avoir plusieurs dédiées à des services spécifiques. Cette machine exécute Debian 12 et est équipée de divers outils comme QEMU, bridge-utils et autres. En combinant ces éléments, on obtient Proxmox.

Pour ce document, je reste dans la simplicité. Proxmox a été installé en suivant ce guide , le stockage est local sur un disque NVMe, un seul pont réseau a été créé (celui par défaut, vmbr0). La version de Proxmox, au moment de l’écriture, est la 8.4.1. Les machines virtuelles seront créées avec les drivers virtio, sans aucune optimisation côté Proxmox ni dans la MV. Le principe est toujours le même : SIMPLICITÉ. Restons le plus simple possible, partout.

Une petite subtilité concernant le stockage des données de Talos : une machine virtuelle sans OS sera créée avec deux disques durs virtuels, qui seront attachés à chacune des machines virtuelles Talos. Chaque machine virtuelle Talos disposera donc de son disque système et d’un disque de données. Ce disque de données n’est pas indispensable ; il est présent pour exploiter l’outil local-path-provisioner et tester différentes fonctionnalités ultérieurement.

Contexte Talos (1.9.4)#

Talos, de Siderolabs, a une approche pragmatique et minimaliste que j’apprécie fortement. Afin de minimiser l’empreinte du système et la complexité, seuls les binaires strictement nécessaires à l’exécution de Kubernetes et du noyau Linux sont présents. Il n’y a ni shell, ni SSH, ni snap : rien de superflu.

L’éditeur fournit les outils requis pour personnaliser l’image ISO ou le disque virtuel de Talos afin de répondre à vos besoins et à la configuration de votre système. En naviguant sur « https://factory.talos.dev », vous trouverez un formulaire qui vous guidera à travers les différentes options. De plus, un « schematic ID » est fourni, un code qui vous permet de conserver les mêmes options et personnalisations entre les versions de Talos.

Ici, j’ai choisi le type de machine “cloud server”, la version 1.9.4 et Nocloud, une architecture AMD64 (sans secureboot), les extensions “amd-ucode ; amdgpu” (parce que mon hyperviseur dispose d’un processeur AMD), “iscsi-tools” et “qemu-guest-agent”, et j’ai saisi les options de noyau suivantes : net.ifnames=0 biosdevname=0 console=tty0 console=ttyS0,115200n8 mitigations=off elevator=none. Ces options de noyau permettent de forcer le nom des cartes réseaux en eth au lieu de noms prédictibles ens, d’afficher les erreurs noyaux directement dans la console (tty0), et de désactiver deux optimisations inutiles dans un contexte virtuel (mitigations et elevator). La page finale de l’assistant vous donnera un résumé de votre configuration et le fameux schematic ID en plus des liens de téléchargement pour l’image ISO ou le disque virtuel.

La gestion simplifiée des machines et des outils Kubernetes constitue un autre avantage. Avec Talos, toutes les dépendances sont à jour et compatibles entre elles. Cependant, il est important de vérifier la compatibilité entre la version Kubernetes et vos CNI, CSI, ainsi que des dépendances comme cert-manager et gateway-api.

Contexte OpenTofu (1.9.0)#

Passons maintenant au code OpenTofu. Je vais créer plusieurs fichiers dans ce projet pour définir les machines virtuelles et le cluster Kubernetes. Je m’excuse par avance si la lisibilité de l’article n’est pas optimale.

OpenTofu facilite l’automatisation et la mise à l’échelle des déploiements de machines, et dans notre cas, de Kubernetes. Grâce à l’utilisation de fichiers de configuration comme talos.tf et vm.tf, il est possible de déployer rapidement de nouveaux nœuds et de mettre à jour les configurations existantes. La mise à l’échelle horizontale peut être réalisée en ajoutant de nouveaux nœuds workers, tandis que la mise à l’échelle verticale peut être effectuée en ajustant les ressources (CPU, mémoire vive, stockage) allouées à chaque nœud.

L’arborescence du projet est la suivante :

opentofu
> talos_nocloud
>> main.tf
>> variables.tf
>> vm.tf
>> talos.tf
>> talos-img.tf

Le fichier main.tf va comporter la déclaration des providers et les valeurs associées :

terraform {
  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "0.76.0"
    }
    talos = {
      source  = "siderolabs/talos"
      version = "0.7.1"
    }
    random = {
      source  = "hashicorp/random"
      version = "3.7.1"
    }
  }
}

provider "proxmox" {
  api_token = "user@realm!token_name=token_id"
  endpoint  = "https://your.proxmox.endpoint:8006/api2/json"
  insecure  = true # because self-signed TLS certificate is in use
  tmp_dir   = "/var/tmp/"

  ssh {
    agent    = true
    username = "root"
  }
}

provider "talos" {}

Le fichier variables.tf comporte la définition des variables qui seront utilisées dans les fichiers vm.tf et talos.tf. J’utiliserai l’approche “Utiliser un fichier .tfvars comportant plusieurs blocs de ressources” du document https://j.hommet.net/deployer-plusieurs-vm-avec-terraform-opentofu-dans-proxmox/ .

variable "kubernetes_version" {
  type = string
}

variable "talos_cluster_endpoint" {
  description = "The endpoint for the Talos cluster"
  type        = string
}
variable "talos_cluster_name" {
  description = "A name to provide for the Talos cluster"
  type        = string
}
variable "talos_version" {
  type = string
}

variable "nodes" {
  description = "Values of VM resources"
  type = map(object({
    data_vm_interface_disk         = string
    pve                            = string
    role                           = string
    usage                          = string
    vm_boot_disk_format            = optional(string, "raw")
    vm_boot_disk_size              = number
    vm_cpu_cores                   = number
    vm_cpu_flags                   = optional(list(string))
    vm_cpu_type                    = optional(string, "x86-64-v2-AES")
    vm_data_disk_interface         = string
    vm_datastore_id                = string
    vm_datastore_id_boot_disk      = string
    vm_datastore_id_data_disk      = string
    vm_datastore_id_efi_disk       = string
    vm_datastore_id_initialization = string
    vm_datastore_id_tpm            = string
    vm_description                 = string
    vm_dns                         = list(string)
    vm_domain                      = string
    vm_efi                         = optional(bool, true)
    vm_eth_rate_limit              = optional(number, 0)
    vm_gateway                     = string
    vm_id                          = number
    vm_install_disk                = string
    vm_ip                          = string
    vm_kvm_args                    = optional(string)
    vm_mac_address                 = string
    vm_memory_dedicated            = number
    vm_memory_floating             = number
    vm_name                        = string
    vm_on_boot                     = optional(bool, true)
    vm_pool_id                     = optional(string)
    vm_startup_order               = optional(number, 1)
    vm_started                     = optional(bool, true)
    vm_tags                        = optional(list(string))
    vm_timeservers                 = optional(list(string), ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"])
    vm_tpm                         = optional(bool, true)
  }))
}

variable "meta_config_metadata" {
  description = "Metadata for cloud-init configuration"
  type = map(object({
    snippet_datastore_id = string
    snippet_pve          = string
    data                 = string
  }))
}

La variable “nodes” est un ensemble de valeurs dans lequel je spécifie les données pour chaque VM. Si vous souhaitez ajouter d’autres machines, il vous suffira de dupliquer un bloc de valeurs de nodes.

Le fichier vm.tf est assez classique (similaire à ce que vous pouvez trouver dans d’autres articles sur mon site) :

# Machines pour le stockage de données statiques pour Talos
resource "proxmox_virtual_environment_vm" "talos_vm_data" {
  description = "Managed by OpenTofu. Talos data disks."
  name        = "k8s-data"
  node_name   = "miniquarium"
  on_boot     = false
  protection  = true
  started     = false
  tags        = sort(["infra", "ne_pas_demarrer", "ne_pas_supprimer"])
  vm_id       = 9000

  disk { # control plane data disk 1
    backup       = true
    datastore_id = "local-nvme"
    file_format  = "raw"
    interface    = "scsi10"
    size         = 48
  }
  disk { # worker 1 data disk 1
    backup       = true
    datastore_id = "local-nvme"
    file_format  = "raw"
    interface    = "scsi11"
    size         = 64
  }
}

# Déclaration de machine virtuelle pour Talos
resource "proxmox_virtual_environment_vm" "talos_vm" {
  depends_on = [
    # proxmox_virtual_environment_download_file.talos_nocloud_image,
    proxmox_virtual_environment_file.meta_cloud_config
  ]
  for_each = var.nodes

  acpi                = true
  bios                = "ovmf"
  description         = each.value.vm_description
  keyboard_layout     = "fr"
  kvm_arguments       = each.value.vm_kvm_args
  machine             = "pc-q35-9.0"
  migrate             = true
  name                = each.value.vm_name
  node_name           = each.value.pve
  on_boot             = each.value.vm_on_boot ? true : false
  pool_id             = each.value.vm_pool_id
  scsi_hardware       = "virtio-scsi-single"
  started             = each.value.vm_started ? "true" : "false"
  stop_on_destroy     = true
  tablet_device       = false
  tags                = each.value.vm_tags
  timeout_create      = 180
  timeout_shutdown_vm = 30
  timeout_stop_vm     = 30
  vm_id               = each.value.vm_id

  agent {
    enabled = true
    timeout = "5m"
    trim    = true
  }

  cpu {
    cores   = each.value.vm_cpu_cores
    flags   = each.value.vm_cpu_flags
    numa    = true
    sockets = 1
    type    = each.value.vm_cpu_type
  }

  disk { # boot disk
    aio          = "native"
    backup       = false
    cache        = "none"
    datastore_id = each.value.vm_datastore_id_boot_disk
    discard      = "on"
    file_format  = each.value.vm_boot_disk_format
    file_id      = "local:iso/talos-v1.9.4-nocloud-amd64.img"
    interface    = "scsi0"
    iothread     = true
    replicate    = false
    size         = each.value.vm_boot_disk_size
  }

  dynamic "disk" { # data disk
    for_each = { for idx, disk in proxmox_virtual_environment_vm.talos_vm_data.disk : idx => disk if disk.interface == each.value.data_vm_interface_disk }
    iterator = data_disk
    content {
      datastore_id = data_disk.value.datastore_id
      discard      = "on"
      file_format  = data_disk.value.file_format
      size         = data_disk.value.size
      interface    = each.value.vm_data_disk_interface
    }
  }

  dynamic "efi_disk" {
    for_each = each.value.vm_efi ? [1] : []
    content {
      datastore_id      = each.value.vm_datastore_id_efi_disk
      file_format       = "raw"
      pre_enrolled_keys = false
      type              = "4m"
    }
  }

  initialization {
    datastore_id = each.value.vm_datastore_id_initialization
    dns {
      domain  = each.value.vm_domain
      servers = each.value.vm_dns
    }
    ip_config {
      ipv4 {
        address = "${each.value.vm_ip}/24"
        gateway = each.value.vm_gateway
      }
      ipv6 {
        address = "dhcp"
      }
    }
  }

  memory {
    dedicated = each.value.vm_memory_dedicated
    floating  = each.value.vm_memory_floating
  }

  network_device {
    bridge      = "vmbr0"
    firewall    = false
    mac_address = each.value.vm_mac_address
    model       = "virtio"
    rate_limit  = each.value.vm_eth_rate_limit
  }

  operating_system {
    type = "l26"
  }

  serial_device {}

  startup {
    order      = each.value.vm_startup_order
    up_delay   = 15
    down_delay = 60
  }

  dynamic "tpm_state" {
    for_each = each.value.vm_tpm ? [1] : []
    content {
      datastore_id = each.value.vm_datastore_id_tpm
      version      = "v2.0"
    }
  }

  vga {
    clipboard = "" # false if empty
    type      = "virtio"
  }
}

resource "proxmox_virtual_environment_file" "meta_cloud_config" {
  for_each = var.meta_config_metadata

  content_type = "snippets"
  datastore_id = each.value.snippet_datastore_id
  node_name    = each.value.snippet_pve

  source_raw {
    data      = each.value.data
    file_name = "${each.key}_ci_meta-data.yml"
  }
}

Passons à un fichier plutôt conséquent, le parameters.auto.tfvars. Modifiez les valeurs selon votre environnement :

kubernetes_version     = "1.31.6"
talos_cluster_name     = "poctalos"
talos_cluster_endpoint = "172.16.255.1"
talos_version          = "1.9.4"

meta_config_metadata = {
  "ber" = {
    snippet_datastore_id = "local"
    snippet_pve          = "pve"
    data                 = <<-EOF
      instance-id: talos-cp
      local-hostname: ber
    EOF
  }

  "nar" = {
    snippet_datastore_id = "local"
    snippet_pve          = "pve"
    data                 = <<-EOF
      instance-id: talos-wkr
      local-hostname: nar
    EOF
  }
}

nodes = {
  "ber" = {
    data_vm_interface_disk = "scsi10" # port on the data VM disk
    pve                    = "pve"
    role                   = "controlplane"
    usage                  = "controlplane"
    vm_boot_disk_format    = "raw"
    vm_boot_disk_size      = 24
    vm_cpu_cores           = 2
    vm_cpu_type                    = "x86-64-v2-AES"
    vm_data_disk_interface         = "scsi1" # port on the control plane VM
    vm_datastore_id                = "local"
    vm_datastore_id_boot_disk      = "local"
    vm_datastore_id_data_disk      = "local"
    vm_datastore_id_efi_disk       = "local"
    vm_datastore_id_initialization = "local"
    vm_datastore_id_tpm            = "local"
    vm_description                 = "Managed by OpenTofu. Talos controlplane"
    vm_dns                         = ["172.16.255.253"]
    vm_domain                      = "dc.home.arpa"
    vm_efi                         = true
    vm_eth_rate_limit              = 0
    vm_gateway                     = "172.16.255.254"
    vm_id                          = 99121
    vm_install_disk                = "/dev/sda"
    vm_ip                          = "172.16.255.1"
    vm_kvm_args                    = ""
    vm_mac_address                 = "BC:24:11:CA:FE:21"
    vm_memory_dedicated            = 6144
    vm_memory_floating             = 6144
    vm_name                        = "ber"
    vm_on_boot                     = true
    vm_pool_id                     = ""
    vm_started                     = true
    vm_startup_order               = 2
    vm_tags                        = ["controlplane", "k8s", "opentofu", "talos"]
    vm_timeservers                 = ["fr.pool.ntp.org", "time.cloudflare.com"]
    vm_tpm                         = true
  }

  "nar" = {
    data_vm_interface_disk = "scsi11" # port on the data VM disk
    pve                    = "pve"
    role                   = "worker"
    usage                  = "infra"
    vm_boot_disk_format    = "raw"
    vm_boot_disk_size      = 24
    vm_cpu_cores           = 3
    vm_cpu_type                    = "x86-64-v2-AES"
    vm_data_disk_interface         = "scsi1" # port on the worker VM
    vm_datastore_id                = "local"
    vm_datastore_id_boot_disk      = "local"
    vm_datastore_id_data_disk      = "local"
    vm_datastore_id_efi_disk       = "local"
    vm_datastore_id_initialization = "local"
    vm_datastore_id_tpm            = "local"
    vm_description                 = "Managed by OpenTofu. Talos worker"
    vm_dns                         = ["172.16.255.253"]
    vm_domain                      = "dc.home.arpa"
    vm_efi                         = true
    vm_eth_rate_limit              = 0
    vm_gateway                     = "172.16.255.254"
    vm_id                          = 99122
    vm_install_disk                = "/dev/sda"
    vm_ip                          = "172.16.255.2"
    vm_kvm_args                    = ""
    vm_mac_address                 = "BC:24:11:CA:FE:22"
    vm_memory_dedicated            = 16384 # 16 GB
    vm_memory_floating             = 16384 # 16 GB
    vm_name                        = "nar"
    vm_on_boot                     = true
    vm_pool_id                     = ""
    vm_started                     = true
    vm_startup_order               = 3
    vm_tags                        = ["k8s", "opentofu", "talos", "worker"]
    vm_timeservers                 = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"]
    vm_tpm                         = true
  }
}

Le fichier talos.tf comporte la déclaration du cluster Kubernetes. En plus, il y a des configurations spécifiques selon le type de nœud, l’ajout de manifests pour installer Cilium (et les configurations nécessaires pour gateway-api et l’annoncement L2), la création des fichiers de configuration pour les control planes et worker. Le code n’est pas entièrement de moi, j’ai repris une partie du travail de Vegard S. Hagen (https://blog.stonegarden.dev/ ) et aussi de Quentin JOLY (https://une-tasse-de.cafe/blog/talos/ ).

# Getting the kubeconfig and talosconfig can be done with "terraform output -raw kubeconfig > $HOME/.kube/config"
# and "terraform output -raw talosconfig > $HOME/.talos/config"".
# source : https://github.com/siderolabs/contrib/tree/main/examples/terraform/basic
# source 2 : https://github.com/vehagn/homelab/blob/main/tofu/kubernetes/talos/config.tf

resource "talos_machine_secrets" "this" {}

data "talos_machine_configuration" "controlplane" {
  for_each = var.nodes

  cluster_endpoint   = "https://${var.talos_cluster_endpoint}:6443"
  cluster_name       = var.talos_cluster_name
  kubernetes_version = var.kubernetes_version
  machine_secrets    = talos_machine_secrets.this.machine_secrets
  machine_type       = "controlplane"
  talos_version      = var.talos_version

  config_patches = [
    yamlencode({
      machine = {
        kubelet = {
          nodeIP = {
            validSubnets = ["172.16.255.0/24"]
          }
        }
        features = {
          hostDNS = {
            enabled              = true
            forwardKubeDNSToHost = false
            resolveMemberNames   = true
          }
        }
        network = {
          hostname    = each.value.vm_name
          nameservers = each.value.vm_dns
          interfaces = [
            {
              addresses = ["${each.value.vm_ip}/24"]
              dhcp      = false
              interface = "eth0"
              routes = [
                {
                  network = "0.0.0.0/0"
                  gateway = each.value.vm_gateway
                }
              ]
            }
          ]
        }
        install = {
          disk = each.value.vm_install_disk
        }
        time = {
          servers = each.value.vm_timeservers
        }
      }
      cluster = {
        etcd = {
          advertisedSubnets = ["172.16.255.0/24"]
        }
        discovery = {
          enabled = false
          registries = {
            service = {
              disabled = true
            }
          }
        }
        network = {
          cni = { # Cilium will replace it
            name = "custom"
            urls = [
              "https://raw.githubusercontent.com/julienhmmt/homelab/refs/heads/main/kubernetes/00-cilium/00-cilium-custom.yaml"
            ]
          }
        }
        proxy = { # Cilium will replace it
          disabled = true
        }
        allowSchedulingOnControlPlanes = false
        extraManifests = [
          # Gateway API
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml",
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gateways.yaml",
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml",
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml",
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_grpcroutes.yaml",
          "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml",
          # Cilium
          "https://raw.githubusercontent.com/julienhmmt/homelab/refs/heads/main/kubernetes/00-cilium/00-cilium-gapi.yaml",
          "https://raw.githubusercontent.com/julienhmmt/homelab/refs/heads/main/kubernetes/00-cilium/00-cilium-l2announcement.yaml"
        ]
      }
    })
  ]
}

resource "talos_machine_configuration_apply" "controlplane" {
  depends_on = [data.talos_machine_configuration.controlplane]
  for_each   = { for key, value in var.nodes : key => value if value.role == "controlplane" }
  lifecycle { replace_triggered_by = [proxmox_virtual_environment_vm.talos_vm[each.key]] } # re-run config apply if vm changes

  client_configuration        = talos_machine_secrets.this.client_configuration
  machine_configuration_input = data.talos_machine_configuration.controlplane[each.key].machine_configuration
  node                        = each.value.vm_ip
}

data "talos_machine_configuration" "worker" {
  for_each = var.nodes

  cluster_endpoint   = "https://${var.talos_cluster_endpoint}:6443"
  cluster_name       = var.talos_cluster_name
  kubernetes_version = var.kubernetes_version
  machine_secrets    = talos_machine_secrets.this.machine_secrets
  machine_type       = "worker"
  talos_version      = var.talos_version

  config_patches = [
    yamlencode({
      machine = {
        network = {
          hostname = each.value.vm_name
        }
        install = {
          disk = each.value.vm_install_disk
        }
        time = {
          servers = ["fr.pool.ntp.org", "time.cloudflare.com"]
        }
      }
    })
  ]
}

resource "talos_machine_configuration_apply" "worker" {
  depends_on = [data.talos_machine_configuration.worker]
  for_each   = { for key, value in var.nodes : key => value if value.role == "worker" }
  lifecycle { replace_triggered_by = [proxmox_virtual_environment_vm.talos_vm[each.key]] } # re-run config apply if vm changes

  client_configuration        = talos_machine_secrets.this.client_configuration
  machine_configuration_input = data.talos_machine_configuration.worker[each.key].machine_configuration
  node                        = each.value.vm_ip
}

data "talos_client_configuration" "this" {
  cluster_name         = var.talos_cluster_name
  endpoints            = [for node in var.nodes : node.vm_ip if node.role == "controlplane"]
  client_configuration = talos_machine_secrets.this.client_configuration
}

resource "talos_machine_bootstrap" "this" {
  depends_on           = [talos_machine_configuration_apply.controlplane]
  client_configuration = talos_machine_secrets.this.client_configuration
  node                 = [for k, v in var.nodes : v.vm_ip if v.role == "controlplane"][0]
}

data "talos_cluster_health" "this" {
  depends_on = [
    talos_machine_configuration_apply.controlplane,
    talos_machine_configuration_apply.worker,
    talos_machine_bootstrap.this
  ]
  endpoints              = data.talos_client_configuration.this.endpoints
  client_configuration   = data.talos_client_configuration.this.client_configuration
  control_plane_nodes    = [for k, v in var.nodes : v.vm_ip if v.role == "controlplane"]
  skip_kubernetes_checks = true
  timeouts = {
    read = "10m"
  }
  worker_nodes = [for k, v in var.nodes : v.vm_ip if v.role == "worker"]
}

resource "talos_cluster_kubeconfig" "this" {
  depends_on = [
    talos_machine_bootstrap.this,
    data.talos_cluster_health.this
  ]
  client_configuration = talos_machine_secrets.this.client_configuration
  endpoint             = var.talos_cluster_endpoint
  node                 = [for k, v in var.nodes : v.vm_ip if v.role == "controlplane"][0]
  timeouts = {
    read = "1m"
  }
}

output "talosconfig" {
  value     = data.talos_client_configuration.this.talos_config
  sensitive = true # Empêche l’affichage en clair dans les logs
}

output "kubeconfig" {
  value     = talos_cluster_kubeconfig.this.kubeconfig_raw
  sensitive = true # Empêche l’affichage en clair dans les logs
}

Quelques explications :

  • La ressource talos_machine_secrets génère les secrets nécessaires pour sécuriser les communications entre les nœuds Talos. Ces secrets sont utilisés pour chiffrer les communications et authentifier les nœuds, garantissant ainsi que seules les machines autorisées peuvent rejoindre le cluster.
  • La ressource talos_machine_configuration configure les nœuds Talos avec des paramètres spécifiques, tels que les configurations réseau, les paramètres Kubernetes, et les patches de configuration. Ces configurations permettent de personnaliser chaque nœud en fonction de son rôle (control plane ou worker) et de ses besoins spécifiques.
  • La ressource talos_machine_configuration_apply applique ensuite ces configurations aux nœuds, assurant que chaque machine est correctement configurée avant de rejoindre le cluster.
  • Cilium est utilisé comme CNI (Container Network Interface). Cilium est choisi pour ses performances élevées et sa capacité à fournir une sécurité réseau avancée grâce à l’utilisation de eBPF. Comparé à d’autres solutions comme Calico ou Flannel, Cilium offre une meilleure visibilité et un contrôle plus granulaire du trafic réseau. Les configurations réseau appliquées incluent la définition des interfaces réseau, des adresses IP, et des routes, garantissant que chaque nœud peut communiquer efficacement au sein du cluster.

OpenTofu exécutera les manifests dans la rubrique extraManifests dans l’ordre indiqué. Par exemple, un manifeste Gateway ne peut être lancé avant GatewayClass, car le type Gateway nécessite la GatewayClass pour être créé. Soyez vigilant quant à l’ordre des lignes.

De plus, dans cette rubrique, j’ajoute les manifests correspondant à gateway-api (qui remplace les ingress). Pour Cilium 1.17.2, Talos 1.9.4 et Kubernetes 1.31.6, la version de Gateway-API à utiliser est la 1.2.0. Voici les commandes :

curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml
curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_gateways.yaml
curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml
curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/standard/gateway.networking.k8s.io_grpcroutes.yaml
curl -O https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.0/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml

J’ajoute également deux autres manifests pour finaliser la configuration gateway-api pour Cilium. L’intérêt de ce bloc réside dans sa capacité à intégrer des manifests pour déployer des applications ou des configurations dans Kubernetes via OpenTofu, sans avoir besoin de l’outil kubectl local. Une fonctionnalité très pratique !

Le dernier fichier du projet, talos-img.tf, est assez simple à comprendre. Il télécharge l’image virtuelle de Talos directement dans le stockage Proxmox.

locals {
  talos = {
    version = "v1.9.4"
    schema  = "ca84112fe212adcded1df4faf04156b49a915b14926c43e19fc09b06be2d06ae"
  }
}

resource "proxmox_virtual_environment_download_file" "talos_nocloud_image" {
  content_type            = "iso"
  datastore_id            = "local"
  node_name               = "miniquarium"
  file_name               = "talos-${local.talos.version}-nocloud-amd64.img"
  url                     = "https://factory.talos.dev/image/${local.talos.schema}/${local.talos.version}/nocloud-amd64.raw.gz"
  decompression_algorithm = "gz"
  overwrite               = false
}

Dans le bloc locals, changez la valeur dans schema pour correspondre à votre besoin et au résultat obtenu suite au formulaire du site factory.talos.dev.

Désormais, vous avez tous les fichiers pour concevoir votre cluster Kubernetes grâce à Talos, dans des machines virtuelles sur votre hôte Proxmox, le tout piloté par OpenTofu. Lancez les commandes tofu plan et tofu apply pour initier la mise en place. Après plusieurs minutes, vous devriez voir les machines en cours d’exécution, Talos démarré et votre cluster Kubernetes en cours d’instanciation. La durée d’installation peut varier selon les performances de vos machines physiques et virtuelles. À titre d’illustration, sur un processeur Ryzen 7 7700, 64 Go de mémoire vive DDR5 et un disque NVMe PCIe de génération 5, le délai total, de la création à la finalisation (instances prêtes à l’emploi et déploiement des applications compris), a été d’environ 3 minutes et 30 secondes.

Enfin, n’oubliez pas de récupérer les fichiers kubeconfig et talosconfig via les commandes tofu output :

mkdir -p ~/.talos ~/.kube
tofu output -raw kubeconfig > ~/.kube/config
tofu output -raw talosconfig > ~/.talos/config

Vous pouvez retrouver une version à jour dans le dépôt GitHub à l’adresse suivante : https://github.com/julienhmmt/homelab/tree/main/opentofu/talos-nocloud .

Et maintenant ?#

Lorsque le déploiement est terminé, contrôler son bon fonctionnement et son accessibilité avec le fichier kubeconfig en faisant un kubectl get nodes ou encore la commande kubectl get pods -n kube-system -o wide. Ces deux commandes vous permettent d’attester du bon fonctionnement du cluster Kubernetes. Les pods peuvent mettre un peu de temps à démarrer et être dans l’état running, notamment ceux de Cilium.

FAQ et résolution des problèmes courants#

Q : Comment résoudre les problèmes de connectivité réseau entre les nœuds Talos ?

R : Vérifiez les configurations réseau dans le fichier parameters.auto.tfvars, notamment les adresses IP, les interfaces réseau, les bridges, et les routes. Assurez-vous que les nœuds peuvent se joindre et que les règles de pare-feu ne bloquent pas le trafic nécessaire.

Q : Comment mettre à jour les configurations des nœuds Talos ?

R : Modifiez les fichiers de configuration dans le projet OpenTofu et appliquez les changements en utilisant les commandes tofu plan et tofu apply. Les nœuds seront automatiquement mis à jour avec les nouvelles configurations.

Q : Comment ajouter de nouveaux nœuds workers au cluster ?

R : Dupliquez un bloc de configuration de nœud dans le fichier parameters.auto.tfvars et appliquez les changements avec tofu plan && tofu apply. Le nouveau nœud sera automatiquement configuré et ajouté au cluster.

Q : Comment surveiller la santé du cluster Talos ?

R : Comme tout cluster Kubernetes, il est indispensable de déployer des outils de supervision comme une pile technique Prometheus + Grafana, VictoriaMetrics, ou encore une pile Elasticsearch + Metricbeat. Vous pouvez aussi utiliser l’outil talosctl:

  • talosctl health : Cette commande exécute un ensemble de vérifications de santé sur tous les nœuds du cluster et donne un aperçu global de l’état du cluster (connectivité, etcd, Kubernetes, etc.)
  • talosctl cluster show : Affiche les informations générales sur le cluster local provisionné, y compris les nœuds, leur rôle et leur état
  • talosctl -n <IP_DU_NOEUD> logs <service> : Récupère les événements d’un nœud. Par exemple, pour etcd : talosctl -n <IP_DU_NOEUD> logs etcd.
  • talosctl -n <IP_DU_NOEUD> containers --kubernetes : Permet de vérifier l’état des pods système Kubernetes (kube-apiserver, kube-controller-manager, etc.) si le control-plane n’est pas encore accessible via kubectl.

Liens complémentaires#

  • 📚 Documentation officielle Talos
  • 🛠️ OpenTofu (Terraform fork)
  • 🖥️ Proxmox VE

Talos Kubernetes on Proxmox using OpenTofuTalos is an immutable operating system designed to only run Kubernetes. The advantage of Talos is an out-of-the-box Kubernetes install, as well as a smaller attack surface, and easier maintenance.StonegardenVegard S. HagenTalos - Un OS immuable pour KubernetesTalos est un système d’exploitation pour Kubernetes. Il est conçu pour être léger, sécurisé et facile à utiliser. Dans cet article, je vais vous présenter Talos et ses particularités.Une tasse de caféQuentin JOLY

Julien HOMMET
19 minutes
3868 mots
tuto