Les deux premiers articles de cette série posent le vocabulaire d’OpenTelemetry et présentent OpenObserve comme backend « agent-friendly ».

Cet article provisionne le cluster Kubernetes Kapsule avec OpenTofu. Les deux articles suivants y installent OpenObserve, puis l’exposent en HTTPS avec Traefik.

Architecture

Le cluster Scaleway Kubernetes Kapsule en plan de contrôle mutualisé (type = "kapsule") est gratuit, sans SLA :

vous ne payez que les nœuds et les ressources associées, à l’heure.

Un Private Network, un pool de deux nœuds, une IP de load balancer réservée, puis Traefik et OpenObserve par Helm.

OpenObserve tourne en mode standalone :

  • SQLite pour les métadonnées,
  • disque pour les données.

Le mode HA demande un stockage objet, Postgres, NATS et plusieurs rôles de nœuds qui dépasse le cadre d’une démo.

Architecture cible : un load balancer Scaleway devant Traefik, qui route vers OpenObserve standalone dans un cluster Kapsule à deux nœudsClientsHTTPS, IPv4 et IPv6443Scaleway · fr-par-1Load balancer LB-SIP v4 et IPv6 réservéesPROXY protocol v2Private Network · cluster Kapsuleplan de contrôle mutualisénamespace traefikTraefikLet’s Encrypt, HTTP-01PVC acme.json · 1 Giscw-bssdnamespace openobserveIngress o2 et middlewareip-allowlist, filtre l’IP sourceo2-openobserve-standaloneOpenObserve v1.1.1 · SQLitePVC data · 30 Gi · scw-bssd-retainPool « main » · 2 nœuds DEV1-Mnœud 1nœud 2
Les requêtes entrent par le load balancer, passent par Traefik et son middleware d’allowlist, puis atteignent OpenObserve. Le volume d’OpenObserve et celui d’ACME sont des volumes Block Storage.

Le schéma montre la cible à la fin des trois articles. Celui-ci en pose le cluster, le Private Network, le pool et les deux IP réservées.

Trois namespaces à plat : openobserve, otel, demo.

Le provider lit les variables d’environnement relatifs aux identifiants scaleway hors des fichiers .tf :

export SCW_ACCESS_KEY=<SCW_ACCESS_KEY>
export SCW_SECRET_KEY=<SCW_SECRET_KEY>
export SCW_DEFAULT_PROJECT_ID=<SCW_PROJECT_ID>
Gestion des variables d’env

Pour ma part, je ne transmets pas ces variables à l’exception du projet id.

J’utilise un outil de stockage des credentials qui transmet automatiquement ces credentials à OpenTofu.

Plusieurs solutions existent, telles que HashiCorp Vault, Pass, GoPass ou encore Teller. À titre personnel, j’utilise AutomicVault dans ce rôle.

Avec AutomicVault, il suffit de lancer la commande suivante pour injecter les secrets nécessaires :

av inject +SCW_ACCESS_KEY +SCW_SECRET_KEY scw info

Décrire le cluster en Terraform

Le provider et la zone sont fixés dans la configuration

Un volume Block Storage n’existe que dans une zone : un nœud d’une autre zone ne peut pas le monter.

Le pool doit donc être créé dans la zone du volume, et changer de zone recrée le pool.

# versions.tf
terraform {
  required_providers {
    scaleway = {
      source  = "scaleway/scaleway"
      version = "~> 2.84"
    }
  }
}

provider "scaleway" {
  zone       = var.zone
  region     = "fr-par"
  project_id = var.project_id
}

Les variables,

project_id vaut null par défaut. Le provider lit alors SCW_DEFAULT_PROJECT_ID.

# variables.tf
variable "project_id" {
  type    = string
  default = null
}

variable "zone" {
  type    = string
  default = "fr-par-1"
}

variable "kubernetes_version" {
  type    = string
  default = "1.37.1"
}

variable "node_type" {
  type    = string
  default = "DEV1-M"
}

variable "running" {
  type    = bool
  default = true
}

variable "node_count" {
  type    = number
  default = 2
}

1.37.1 vient de la doc du provider. Listez les versions proposées avant d’appliquer :

scw k8s version list

La ressource principale

# main.tf
resource "scaleway_vpc_private_network" "main" {
  name = "observabilite"
}

resource "scaleway_k8s_cluster" "main" {
  name                        = "observabilite"
  type                        = "kapsule"
  version                     = var.kubernetes_version
  cni                         = "cilium"
  private_network_id          = scaleway_vpc_private_network.main.id
  delete_additional_resources = false
}

resource "scaleway_k8s_pool" "main" {
  count      = var.running ? 1 : 0
  cluster_id = scaleway_k8s_cluster.main.id
  name       = "main"
  node_type  = var.node_type
  size       = var.node_count
  zone       = var.zone
}

resource "scaleway_lb_ip" "traefik" {
  zone = var.zone
}

resource "scaleway_lb_ip" "traefik_v6" {
  zone    = var.zone
  is_ipv6 = true
}

Le Private Network est obligatoire sur un cluster Kapsule. delete_additional_resources = false permet de conserver les volumes à la destruction du cluster, mais orphelins : les rattacher demande des PV statiques.

Le cluster n’est donc jamais détruit dans cet article.

Les sorties

# outputs.tf
output "kubeconfig" {
  value     = scaleway_k8s_cluster.main.kubeconfig[0].config_file
  sensitive = true
}

output "lb_ip_id" {
  value = split("/", scaleway_lb_ip.traefik.id)[1]
}

output "lb_ip_address" {
  value = scaleway_lb_ip.traefik.ip_address
}

output "lb_ipv6_id" {
  value = split("/", scaleway_lb_ip.traefik_v6.id)[1]
}

output "lb_ipv6_address" {
  value = scaleway_lb_ip.traefik_v6.ip_address
}

output "pod_cidr" {
  value = scaleway_k8s_cluster.main.pod_cidr
}

output "private_network_cidr" {
  value = scaleway_vpc_private_network.main.ipv4_subnet[0].subnet
}

L’identifiant d’une scaleway_lb_ip a la forme {zone}/{uuid}.
L’annotation du load balancer documente un <ip-id> nu, d’où le split.
La seconde IP, avec is_ipv6 = true, donne l’adresse du futur enregistrement AAAA.

Les commandes utilisent OpenTofu, dont les fichiers et les commandes sont ceux de Terraform.

Pour ne pas passer -var project_id=... à chaque commande, placez l’identifiant du projet dans un fichier project.auto.tfvars.

OpenTofu charge automatiquement les fichiers *.auto.tfvars, alors qu’un fichier nommé project.tfvars demande -var-file.

# project.auto.tfvars
project_id = "<SCW_PROJECT_ID>"

Création du cluster

printf '%s\n' kubeconfig.yaml terraform.tfstate terraform.tfstate.backup .terraform/ '*.tfvars' >> .gitignore
tofu init
tofu apply
tofu output -raw kubeconfig > kubeconfig.yaml
chmod 600 kubeconfig.yaml
export KUBECONFIG=$PWD/kubeconfig.yaml

Appliquez, puis écrivez le kubeconfig dans un fichier local.

Il contient un token, et l’état Terraform le contient aussi : les deux restent hors de git.

Créez maintenant l’enregistrement DNS A de o2.example.com vers lb_ip_address, et l’enregistrement AAAA vers lb_ipv6_address.

Les IPs étant réservées, ils ne bougent plus entre deux sessions.

Dans cet article, j’utiliserai o2.obs.scw.clevertechware.fr au lieu o2.example.com.

Mesurer ce qui reste sur les nœuds

Scaleway ne publie pas ce que Kapsule réserve sur chaque nœud, donc mesurez-le :

kubectl get nodes
kubectl describe node | grep -A6 Allocatable

Deux DEV1-M offrent 2 × 4 Go de RAM.

C’est le plus petit choix qui passe : les types à moins de 4 Go (DEV1-S, PLAY2-PICO, STARDUST) sont refusés.

Pour cet article et le suivant, OpenObserve, Traefik et le Collector tiennent dans ce budget.

Dans un prochain article, il faudra ajouter Postgres et trois applications dont une JVM. On estime le besoin à 5 à 6 Go hors pods système : on passera alors en DEV1-L.

Changer node_type recrée le pool, mais les volumes restent.

Éteindre le cluster entre deux sessions

Supprimer le pool avec running=false

La variable running pilote le count de scaleway_k8s_pool : running=false supprime le pool, et le cluster reste.

tofu apply -var running=false
tofu apply -var running=true

Un cluster Kapsule au plan de contrôle mutualisé n’accepte pas un pool à zéro nœud : -var node_count=0 échoue avec a mutualized cluster cannot have less than 1 node. D’où la suppression du pool entier.

Sans pool, le cluster passe à l’état pool_required et le nom d’hôte de son API ne se résout plus : kubectl échoue jusqu’au rallumage. Les deux IP de load balancer restent réservées. Le plan de contrôle mutualisé ne coûte rien, et les nœuds ne sont facturés que tant que le pool existe.

Ressources